@jsenv/server
Advanced tools
Comparing version 15.0.2 to 15.0.3
{ | ||
"name": "@jsenv/server", | ||
"version": "15.0.2", | ||
"version": "15.0.3", | ||
"description": "Write your Node.js server using pure functions", | ||
@@ -13,3 +13,3 @@ "license": "MIT", | ||
"type": "git", | ||
"url": "https://github.com/jsenv/jsenv-core", | ||
"url": "https://github.com/jsenv/core", | ||
"directory": "packages/server" | ||
@@ -39,3 +39,3 @@ }, | ||
"@jsenv/abort": "4.2.4", | ||
"@jsenv/log": "3.3.4", | ||
"@jsenv/log": "3.3.5", | ||
"@jsenv/url-meta": "8.1.0", | ||
@@ -42,0 +42,0 @@ "@jsenv/utils": "2.0.1", |
@@ -6,3 +6,3 @@ # server [](https://www.npmjs.com/package/@jsenv/server) | ||
```js | ||
import { startServer } from "@jsenv/server" | ||
import { startServer } from "@jsenv/server"; | ||
@@ -20,7 +20,7 @@ await startServer({ | ||
body: "Hello world", | ||
} | ||
}; | ||
}, | ||
}, | ||
], | ||
}) | ||
}); | ||
``` | ||
@@ -40,3 +40,3 @@ | ||
*/ | ||
import { startServer, composeServices } from "@jsenv/server" | ||
import { startServer, composeServices } from "@jsenv/server"; | ||
@@ -49,5 +49,5 @@ const server = await startServer({ | ||
if (request.resource === "/") { | ||
return { status: 200 } | ||
return { status: 200 }; | ||
} | ||
return null | ||
return null; | ||
}, | ||
@@ -58,14 +58,14 @@ }, | ||
handleRequest: () => { | ||
return { status: 404 } | ||
return { status: 404 }; | ||
}, | ||
}, | ||
], | ||
}) | ||
}); | ||
const fetch = await import("node-fetch") | ||
const responseForOrigin = await fetch(server.origin) | ||
responseForOrigin.status // 200 | ||
const fetch = await import("node-fetch"); | ||
const responseForOrigin = await fetch(server.origin); | ||
responseForOrigin.status; // 200 | ||
const responseForFoo = await fetch(`${server.origin}/foo`) | ||
responseForFoo.status // 404 | ||
const responseForFoo = await fetch(`${server.origin}/foo`); | ||
responseForFoo.status; // 404 | ||
``` | ||
@@ -76,4 +76,4 @@ | ||
```js | ||
import { readFileSync } from "node:fs" | ||
import { startServer } from "@jsenv/server" | ||
import { readFileSync } from "node:fs"; | ||
import { startServer } from "@jsenv/server"; | ||
@@ -89,3 +89,3 @@ await startServer({ | ||
handleRequest: (request) => { | ||
const clientUsesHttp = request.origin.startsWith("http:") | ||
const clientUsesHttp = request.origin.startsWith("http:"); | ||
@@ -98,7 +98,7 @@ return { | ||
body: clientUsesHttp ? `Welcome http user` : `Welcome https user`, | ||
} | ||
}; | ||
}, | ||
}, | ||
], | ||
}) | ||
}); | ||
``` | ||
@@ -109,3 +109,3 @@ | ||
```js | ||
import { startServer, fetchFileSystem } from "@jsenv/server" | ||
import { startServer, fetchFileSystem } from "@jsenv/server"; | ||
@@ -116,9 +116,9 @@ await startServer({ | ||
handleRequest: async (request) => { | ||
const fileUrl = new URL(request.resource.slice(1), import.meta.url) | ||
const response = await fetchFileSystem(fileUrl, request) | ||
return response | ||
const fileUrl = new URL(request.resource.slice(1), import.meta.url); | ||
const response = await fetchFileSystem(fileUrl, request); | ||
return response; | ||
}, | ||
}, | ||
], | ||
}) | ||
}); | ||
``` | ||
@@ -125,0 +125,0 @@ |
@@ -6,22 +6,22 @@ export const pickAcceptedContent = ({ | ||
}) => { | ||
let highestScore = -1 | ||
let availableWithHighestScore = null | ||
let availableIndex = 0 | ||
let highestScore = -1; | ||
let availableWithHighestScore = null; | ||
let availableIndex = 0; | ||
while (availableIndex < availables.length) { | ||
const available = availables[availableIndex] | ||
availableIndex++ | ||
const available = availables[availableIndex]; | ||
availableIndex++; | ||
let acceptedIndex = 0 | ||
let acceptedIndex = 0; | ||
while (acceptedIndex < accepteds.length) { | ||
const accepted = accepteds[acceptedIndex] | ||
acceptedIndex++ | ||
const accepted = accepteds[acceptedIndex]; | ||
acceptedIndex++; | ||
const score = getAcceptanceScore(accepted, available) | ||
const score = getAcceptanceScore(accepted, available); | ||
if (score > highestScore) { | ||
availableWithHighestScore = available | ||
highestScore = score | ||
availableWithHighestScore = available; | ||
highestScore = score; | ||
} | ||
} | ||
} | ||
return availableWithHighestScore | ||
} | ||
return availableWithHighestScore; | ||
}; |
@@ -1,9 +0,9 @@ | ||
import { parseMultipleHeader } from "../internal/multiple-header.js" | ||
import { pickAcceptedContent } from "./pick_accepted_content.js" | ||
import { parseMultipleHeader } from "../internal/multiple-header.js"; | ||
import { pickAcceptedContent } from "./pick_accepted_content.js"; | ||
export const pickContentEncoding = (request, availableEncodings) => { | ||
const { headers = {} } = request | ||
const requestAcceptEncodingHeader = headers["accept-encoding"] | ||
const { headers = {} } = request; | ||
const requestAcceptEncodingHeader = headers["accept-encoding"]; | ||
if (!requestAcceptEncodingHeader) { | ||
return null | ||
return null; | ||
} | ||
@@ -13,3 +13,3 @@ | ||
requestAcceptEncodingHeader, | ||
) | ||
); | ||
return pickAcceptedContent({ | ||
@@ -19,4 +19,4 @@ accepteds: encodingsAccepted, | ||
getAcceptanceScore: getEncodingAcceptanceScore, | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -27,35 +27,35 @@ const parseAcceptEncodingHeader = (acceptEncodingHeaderString) => { | ||
// read only q, anything else is ignored | ||
return name === "q" | ||
return name === "q"; | ||
}, | ||
}) | ||
}); | ||
const encodingsAccepted = [] | ||
const encodingsAccepted = []; | ||
Object.keys(acceptEncodingHeader).forEach((key) => { | ||
const { q = 1 } = acceptEncodingHeader[key] | ||
const value = key | ||
const { q = 1 } = acceptEncodingHeader[key]; | ||
const value = key; | ||
encodingsAccepted.push({ | ||
value, | ||
quality: q, | ||
}) | ||
}) | ||
}); | ||
}); | ||
encodingsAccepted.sort((a, b) => { | ||
return b.quality - a.quality | ||
}) | ||
return encodingsAccepted | ||
} | ||
return b.quality - a.quality; | ||
}); | ||
return encodingsAccepted; | ||
}; | ||
const getEncodingAcceptanceScore = ({ value, quality }, availableEncoding) => { | ||
if (value === "*") { | ||
return quality | ||
return quality; | ||
} | ||
// normalize br to brotli | ||
if (value === "br") value = "brotli" | ||
if (availableEncoding === "br") availableEncoding = "brotli" | ||
if (value === "br") value = "brotli"; | ||
if (availableEncoding === "br") availableEncoding = "brotli"; | ||
if (value === availableEncoding) { | ||
return quality | ||
return quality; | ||
} | ||
return -1 | ||
} | ||
return -1; | ||
}; |
@@ -1,9 +0,9 @@ | ||
import { parseMultipleHeader } from "../internal/multiple-header.js" | ||
import { pickAcceptedContent } from "./pick_accepted_content.js" | ||
import { parseMultipleHeader } from "../internal/multiple-header.js"; | ||
import { pickAcceptedContent } from "./pick_accepted_content.js"; | ||
export const pickContentLanguage = (request, availableLanguages) => { | ||
const { headers = {} } = request | ||
const requestAcceptLanguageHeader = headers["accept-language"] | ||
const { headers = {} } = request; | ||
const requestAcceptLanguageHeader = headers["accept-language"]; | ||
if (!requestAcceptLanguageHeader) { | ||
return null | ||
return null; | ||
} | ||
@@ -13,3 +13,3 @@ | ||
requestAcceptLanguageHeader, | ||
) | ||
); | ||
return pickAcceptedContent({ | ||
@@ -19,4 +19,4 @@ accepteds: languagesAccepted, | ||
getAcceptanceScore: getLanguageAcceptanceScore, | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -27,54 +27,55 @@ const parseAcceptLanguageHeader = (acceptLanguageHeaderString) => { | ||
// read only q, anything else is ignored | ||
return name === "q" | ||
return name === "q"; | ||
}, | ||
}) | ||
}); | ||
const languagesAccepted = [] | ||
const languagesAccepted = []; | ||
Object.keys(acceptLanguageHeader).forEach((key) => { | ||
const { q = 1 } = acceptLanguageHeader[key] | ||
const value = key | ||
const { q = 1 } = acceptLanguageHeader[key]; | ||
const value = key; | ||
languagesAccepted.push({ | ||
value, | ||
quality: q, | ||
}) | ||
}) | ||
}); | ||
}); | ||
languagesAccepted.sort((a, b) => { | ||
return b.quality - a.quality | ||
}) | ||
return languagesAccepted | ||
} | ||
return b.quality - a.quality; | ||
}); | ||
return languagesAccepted; | ||
}; | ||
const getLanguageAcceptanceScore = ({ value, quality }, availableLanguage) => { | ||
const [acceptedPrimary, acceptedVariant] = decomposeLanguage(value) | ||
const [acceptedPrimary, acceptedVariant] = decomposeLanguage(value); | ||
const [availablePrimary, availableVariant] = | ||
decomposeLanguage(availableLanguage) | ||
decomposeLanguage(availableLanguage); | ||
const primaryAccepted = | ||
acceptedPrimary === "*" || | ||
acceptedPrimary.toLowerCase() === availablePrimary.toLowerCase() | ||
acceptedPrimary.toLowerCase() === availablePrimary.toLowerCase(); | ||
const variantAccepted = | ||
acceptedVariant === "*" || compareVariant(acceptedVariant, availableVariant) | ||
acceptedVariant === "*" || | ||
compareVariant(acceptedVariant, availableVariant); | ||
if (primaryAccepted && variantAccepted) { | ||
return quality + 1 | ||
return quality + 1; | ||
} | ||
if (primaryAccepted) { | ||
return quality | ||
return quality; | ||
} | ||
return -1 | ||
} | ||
return -1; | ||
}; | ||
const decomposeLanguage = (fullType) => { | ||
const [primary, variant] = fullType.split("-") | ||
return [primary, variant] | ||
} | ||
const [primary, variant] = fullType.split("-"); | ||
return [primary, variant]; | ||
}; | ||
const compareVariant = (left, right) => { | ||
if (left === right) { | ||
return true | ||
return true; | ||
} | ||
if (left && right && left.toLowerCase() === right.toLowerCase()) { | ||
return true | ||
return true; | ||
} | ||
return false | ||
} | ||
return false; | ||
}; |
@@ -1,12 +0,12 @@ | ||
import { parseMultipleHeader } from "../internal/multiple-header.js" | ||
import { pickAcceptedContent } from "./pick_accepted_content.js" | ||
import { parseMultipleHeader } from "../internal/multiple-header.js"; | ||
import { pickAcceptedContent } from "./pick_accepted_content.js"; | ||
export const pickContentType = (request, availableContentTypes) => { | ||
const { headers = {} } = request | ||
const requestAcceptHeader = headers.accept | ||
const { headers = {} } = request; | ||
const requestAcceptHeader = headers.accept; | ||
if (!requestAcceptHeader) { | ||
return null | ||
return null; | ||
} | ||
const contentTypesAccepted = parseAcceptHeader(requestAcceptHeader) | ||
const contentTypesAccepted = parseAcceptHeader(requestAcceptHeader); | ||
return pickAcceptedContent({ | ||
@@ -16,4 +16,4 @@ accepteds: contentTypesAccepted, | ||
getAcceptanceScore: getContentTypeAcceptanceScore, | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -24,20 +24,20 @@ const parseAcceptHeader = (acceptHeader) => { | ||
// read only q, anything else is ignored | ||
return name === "q" | ||
return name === "q"; | ||
}, | ||
}) | ||
}); | ||
const accepts = [] | ||
const accepts = []; | ||
Object.keys(acceptHeaderObject).forEach((key) => { | ||
const { q = 1 } = acceptHeaderObject[key] | ||
const value = key | ||
const { q = 1 } = acceptHeaderObject[key]; | ||
const value = key; | ||
accepts.push({ | ||
value, | ||
quality: q, | ||
}) | ||
}) | ||
}); | ||
}); | ||
accepts.sort((a, b) => { | ||
return b.quality - a.quality | ||
}) | ||
return accepts | ||
} | ||
return b.quality - a.quality; | ||
}); | ||
return accepts; | ||
}; | ||
@@ -48,19 +48,19 @@ const getContentTypeAcceptanceScore = ( | ||
) => { | ||
const [acceptedType, acceptedSubtype] = decomposeContentType(value) | ||
const [acceptedType, acceptedSubtype] = decomposeContentType(value); | ||
const [availableType, availableSubtype] = | ||
decomposeContentType(availableContentType) | ||
decomposeContentType(availableContentType); | ||
const typeAccepted = acceptedType === "*" || acceptedType === availableType | ||
const typeAccepted = acceptedType === "*" || acceptedType === availableType; | ||
const subtypeAccepted = | ||
acceptedSubtype === "*" || acceptedSubtype === availableSubtype | ||
acceptedSubtype === "*" || acceptedSubtype === availableSubtype; | ||
if (typeAccepted && subtypeAccepted) { | ||
return quality | ||
return quality; | ||
} | ||
return -1 | ||
} | ||
return -1; | ||
}; | ||
const decomposeContentType = (fullType) => { | ||
const [type, subtype] = fullType.split("/") | ||
return [type, subtype] | ||
} | ||
const [type, subtype] = fullType.split("/"); | ||
return [type, subtype]; | ||
}; |
@@ -7,12 +7,15 @@ /* | ||
import { createReadStream, statSync, readFile } from "node:fs" | ||
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js" | ||
import { createReadStream, statSync, readFile } from "node:fs"; | ||
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js"; | ||
import { isFileSystemPath, fileSystemPathToUrl } from "./internal/filesystem.js" | ||
import { bufferToEtag } from "./internal/etag.js" | ||
import { composeTwoResponses } from "./internal/response_composition.js" | ||
import { convertFileSystemErrorToResponseProperties } from "./internal/convertFileSystemErrorToResponseProperties.js" | ||
import { timeFunction } from "./server_timing/timing_measure.js" | ||
import { pickContentEncoding } from "./content_negotiation/pick_content_encoding.js" | ||
import { serveDirectory } from "./serve_directory.js" | ||
import { | ||
isFileSystemPath, | ||
fileSystemPathToUrl, | ||
} from "./internal/filesystem.js"; | ||
import { bufferToEtag } from "./internal/etag.js"; | ||
import { composeTwoResponses } from "./internal/response_composition.js"; | ||
import { convertFileSystemErrorToResponseProperties } from "./internal/convertFileSystemErrorToResponseProperties.js"; | ||
import { timeFunction } from "./server_timing/timing_measure.js"; | ||
import { pickContentEncoding } from "./content_negotiation/pick_content_encoding.js"; | ||
import { serveDirectory } from "./serve_directory.js"; | ||
@@ -38,7 +41,7 @@ export const fetchFileSystem = async ( | ||
) => { | ||
const urlString = asUrlString(filesystemUrl) | ||
const urlString = asUrlString(filesystemUrl); | ||
if (!urlString) { | ||
return create500Response( | ||
`fetchFileSystem first parameter must be a file url, got ${filesystemUrl}`, | ||
) | ||
); | ||
} | ||
@@ -48,13 +51,13 @@ if (!urlString.startsWith("file://")) { | ||
`fetchFileSystem url must use "file://" scheme, got ${filesystemUrl}`, | ||
) | ||
); | ||
} | ||
if (rootDirectoryUrl) { | ||
let rootDirectoryUrlString = asUrlString(rootDirectoryUrl) | ||
let rootDirectoryUrlString = asUrlString(rootDirectoryUrl); | ||
if (!rootDirectoryUrlString) { | ||
return create500Response( | ||
`rootDirectoryUrl must be a string or an url, got ${rootDirectoryUrl}`, | ||
) | ||
); | ||
} | ||
if (!rootDirectoryUrlString.endsWith("/")) { | ||
rootDirectoryUrlString = `${rootDirectoryUrlString}/` | ||
rootDirectoryUrlString = `${rootDirectoryUrlString}/`; | ||
} | ||
@@ -64,5 +67,5 @@ if (!urlString.startsWith(rootDirectoryUrlString)) { | ||
`fetchFileSystem url must be inside root directory, got ${urlString}`, | ||
) | ||
); | ||
} | ||
rootDirectoryUrl = rootDirectoryUrlString | ||
rootDirectoryUrl = rootDirectoryUrlString; | ||
} | ||
@@ -75,8 +78,8 @@ | ||
if (etagEnabled) { | ||
console.warn(`cannot enable etag when cache-control is ${cacheControl}`) | ||
etagEnabled = false | ||
console.warn(`cannot enable etag when cache-control is ${cacheControl}`); | ||
etagEnabled = false; | ||
} | ||
if (mtimeEnabled) { | ||
console.warn(`cannot enable mtime when cache-control is ${cacheControl}`) | ||
mtimeEnabled = false | ||
console.warn(`cannot enable mtime when cache-control is ${cacheControl}`); | ||
mtimeEnabled = false; | ||
} | ||
@@ -87,4 +90,4 @@ } | ||
`cannot enable both etag and mtime, mtime disabled in favor of etag.`, | ||
) | ||
mtimeEnabled = false | ||
); | ||
mtimeEnabled = false; | ||
} | ||
@@ -95,6 +98,6 @@ | ||
status: 501, | ||
} | ||
}; | ||
} | ||
const sourceUrl = `file://${new URL(urlString).pathname}` | ||
const sourceUrl = `file://${new URL(urlString).pathname}`; | ||
try { | ||
@@ -104,3 +107,3 @@ const [readStatTiming, sourceStat] = await timeFunction( | ||
() => statSync(new URL(sourceUrl)), | ||
) | ||
); | ||
if (sourceStat.isDirectory()) { | ||
@@ -112,3 +115,3 @@ if (canReadDirectory) { | ||
rootDirectoryUrl, | ||
}) | ||
}); | ||
} | ||
@@ -118,3 +121,3 @@ return { | ||
statusText: "not allowed to read directory", | ||
} | ||
}; | ||
} | ||
@@ -126,3 +129,3 @@ // not a file, give up | ||
timing: readStatTiming, | ||
} | ||
}; | ||
} | ||
@@ -138,3 +141,3 @@ | ||
sourceUrl, | ||
}) | ||
}); | ||
@@ -152,6 +155,6 @@ // send 304 (redirect response to client cache) | ||
clientCacheResponse, | ||
) | ||
); | ||
} | ||
let response | ||
let response; | ||
if (compressionEnabled && sourceStat.size >= compressionSizeThreshold) { | ||
@@ -161,5 +164,5 @@ const compressedResponse = await getCompressedResponse({ | ||
sourceUrl, | ||
}) | ||
}); | ||
if (compressedResponse) { | ||
response = compressedResponse | ||
response = compressedResponse; | ||
} | ||
@@ -171,3 +174,3 @@ } | ||
sourceUrl, | ||
}) | ||
}); | ||
} | ||
@@ -189,4 +192,4 @@ | ||
response, | ||
) | ||
return composeTwoResponses(intermediateResponse, clientCacheResponse) | ||
); | ||
return composeTwoResponses(intermediateResponse, clientCacheResponse); | ||
} catch (e) { | ||
@@ -200,5 +203,5 @@ return composeTwoResponses( | ||
convertFileSystemErrorToResponseProperties(e) || {}, | ||
) | ||
); | ||
} | ||
} | ||
}; | ||
@@ -213,4 +216,4 @@ const create500Response = (message) => { | ||
body: message, | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -235,3 +238,3 @@ const getClientCacheResponse = async ({ | ||
) { | ||
return { status: 200 } | ||
return { status: 200 }; | ||
} | ||
@@ -246,3 +249,3 @@ | ||
sourceUrl, | ||
}) | ||
}); | ||
} | ||
@@ -254,7 +257,7 @@ | ||
sourceStat, | ||
}) | ||
}); | ||
} | ||
return { status: 200 } | ||
} | ||
return { status: 200 }; | ||
}; | ||
@@ -277,5 +280,5 @@ const getEtagResponse = async ({ | ||
}), | ||
) | ||
); | ||
const requestHasIfNoneMatchHeader = "if-none-match" in headers | ||
const requestHasIfNoneMatchHeader = "if-none-match" in headers; | ||
if ( | ||
@@ -288,3 +291,3 @@ requestHasIfNoneMatchHeader && | ||
timing: computeEtagTiming, | ||
} | ||
}; | ||
} | ||
@@ -298,6 +301,6 @@ | ||
timing: computeEtagTiming, | ||
} | ||
} | ||
}; | ||
}; | ||
const ETAG_MEMORY_MAP = new Map() | ||
const ETAG_MEMORY_MAP = new Map(); | ||
const computeEtag = async ({ | ||
@@ -310,3 +313,3 @@ etagMemory, | ||
if (etagMemory) { | ||
const etagMemoryEntry = ETAG_MEMORY_MAP.get(sourceUrl) | ||
const etagMemoryEntry = ETAG_MEMORY_MAP.get(sourceUrl); | ||
if ( | ||
@@ -316,3 +319,3 @@ etagMemoryEntry && | ||
) { | ||
return etagMemoryEntry.eTag | ||
return etagMemoryEntry.eTag; | ||
} | ||
@@ -323,18 +326,18 @@ } | ||
if (error) { | ||
reject(error) | ||
reject(error); | ||
} else { | ||
resolve(buffer) | ||
resolve(buffer); | ||
} | ||
}) | ||
}) | ||
const eTag = bufferToEtag(fileContentAsBuffer) | ||
}); | ||
}); | ||
const eTag = bufferToEtag(fileContentAsBuffer); | ||
if (etagMemory) { | ||
if (ETAG_MEMORY_MAP.size >= etagMemoryMaxSize) { | ||
const firstKey = Array.from(ETAG_MEMORY_MAP.keys())[0] | ||
ETAG_MEMORY_MAP.delete(firstKey) | ||
const firstKey = Array.from(ETAG_MEMORY_MAP.keys())[0]; | ||
ETAG_MEMORY_MAP.delete(firstKey); | ||
} | ||
ETAG_MEMORY_MAP.set(sourceUrl, { sourceStat, eTag }) | ||
ETAG_MEMORY_MAP.set(sourceUrl, { sourceStat, eTag }); | ||
} | ||
return eTag | ||
} | ||
return eTag; | ||
}; | ||
@@ -344,7 +347,7 @@ // https://nodejs.org/api/fs.html#fs_class_fs_stats | ||
return fileStatKeysToCompare.every((keyToCompare) => { | ||
const leftValue = leftFileStat[keyToCompare] | ||
const rightValue = rightFileStat[keyToCompare] | ||
return leftValue === rightValue | ||
}) | ||
} | ||
const leftValue = leftFileStat[keyToCompare]; | ||
const rightValue = rightFileStat[keyToCompare]; | ||
return leftValue === rightValue; | ||
}); | ||
}; | ||
const fileStatKeysToCompare = [ | ||
@@ -360,9 +363,9 @@ // mtime the the most likely to change, check it first | ||
"blksize", | ||
] | ||
]; | ||
const getMtimeResponse = async ({ headers, sourceStat }) => { | ||
if ("if-modified-since" in headers) { | ||
let cachedModificationDate | ||
let cachedModificationDate; | ||
try { | ||
cachedModificationDate = new Date(headers["if-modified-since"]) | ||
cachedModificationDate = new Date(headers["if-modified-since"]); | ||
} catch (e) { | ||
@@ -372,10 +375,10 @@ return { | ||
statusText: "if-modified-since header is not a valid date", | ||
} | ||
}; | ||
} | ||
const actualModificationDate = dateToSecondsPrecision(sourceStat.mtime) | ||
const actualModificationDate = dateToSecondsPrecision(sourceStat.mtime); | ||
if (Number(cachedModificationDate) >= Number(actualModificationDate)) { | ||
return { | ||
status: 304, | ||
} | ||
}; | ||
} | ||
@@ -389,4 +392,4 @@ } | ||
}, | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -397,11 +400,11 @@ const getCompressedResponse = async ({ sourceUrl, headers }) => { | ||
Object.keys(availableCompressionFormats), | ||
) | ||
); | ||
if (!acceptedCompressionFormat) { | ||
return null | ||
return null; | ||
} | ||
const fileReadableStream = fileUrlToReadableStream(sourceUrl) | ||
const fileReadableStream = fileUrlToReadableStream(sourceUrl); | ||
const body = await availableCompressionFormats[acceptedCompressionFormat]( | ||
fileReadableStream, | ||
) | ||
); | ||
@@ -416,4 +419,4 @@ return { | ||
body, | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -424,19 +427,19 @@ const fileUrlToReadableStream = (fileUrl) => { | ||
autoClose: true, | ||
}) | ||
} | ||
}); | ||
}; | ||
const availableCompressionFormats = { | ||
br: async (fileReadableStream) => { | ||
const { createBrotliCompress } = await import("node:zlib") | ||
return fileReadableStream.pipe(createBrotliCompress()) | ||
const { createBrotliCompress } = await import("node:zlib"); | ||
return fileReadableStream.pipe(createBrotliCompress()); | ||
}, | ||
deflate: async (fileReadableStream) => { | ||
const { createDeflate } = await import("node:zlib") | ||
return fileReadableStream.pipe(createDeflate()) | ||
const { createDeflate } = await import("node:zlib"); | ||
return fileReadableStream.pipe(createDeflate()); | ||
}, | ||
gzip: async (fileReadableStream) => { | ||
const { createGzip } = await import("node:zlib") | ||
return fileReadableStream.pipe(createGzip()) | ||
const { createGzip } = await import("node:zlib"); | ||
return fileReadableStream.pipe(createGzip()); | ||
}, | ||
} | ||
}; | ||
@@ -451,30 +454,30 @@ const getRawResponse = async ({ sourceUrl, sourceStat }) => { | ||
body: fileUrlToReadableStream(sourceUrl), | ||
} | ||
} | ||
}; | ||
}; | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toUTCString | ||
const dateToUTCString = (date) => date.toUTCString() | ||
const dateToUTCString = (date) => date.toUTCString(); | ||
const dateToSecondsPrecision = (date) => { | ||
const dateWithSecondsPrecision = new Date(date) | ||
dateWithSecondsPrecision.setMilliseconds(0) | ||
return dateWithSecondsPrecision | ||
} | ||
const dateWithSecondsPrecision = new Date(date); | ||
dateWithSecondsPrecision.setMilliseconds(0); | ||
return dateWithSecondsPrecision; | ||
}; | ||
const asUrlString = (value) => { | ||
if (value instanceof URL) { | ||
return value.href | ||
return value.href; | ||
} | ||
if (typeof value === "string") { | ||
if (isFileSystemPath(value)) { | ||
return fileSystemPathToUrl(value) | ||
return fileSystemPathToUrl(value); | ||
} | ||
try { | ||
const urlObject = new URL(value) | ||
return String(urlObject) | ||
const urlObject = new URL(value); | ||
return String(urlObject); | ||
} catch (e) { | ||
return null | ||
return null; | ||
} | ||
} | ||
return null | ||
} | ||
return null; | ||
}; |
export const fromFetchResponse = (fetchResponse) => { | ||
const responseHeaders = {} | ||
const headersToIgnore = ["connection"] | ||
const responseHeaders = {}; | ||
const headersToIgnore = ["connection"]; | ||
fetchResponse.headers.forEach((value, name) => { | ||
if (!headersToIgnore.includes(name)) { | ||
responseHeaders[name] = value | ||
responseHeaders[name] = value; | ||
} | ||
}) | ||
}); | ||
return { | ||
@@ -14,3 +14,3 @@ status: fetchResponse.status, | ||
body: fetchResponse.body, // node-fetch assumed | ||
} | ||
} | ||
}; | ||
}; |
@@ -1,6 +0,6 @@ | ||
import { Stream, Writable, Readable } from "node:stream" | ||
import { createReadStream } from "node:fs" | ||
import { Stream, Writable, Readable } from "node:stream"; | ||
import { createReadStream } from "node:fs"; | ||
import { isObservable, observableFromValue } from "./observable.js" | ||
import { observableFromNodeStream } from "./observable_from_node_stream.js" | ||
import { isObservable, observableFromValue } from "./observable.js"; | ||
import { observableFromNodeStream } from "./observable_from_node_stream.js"; | ||
@@ -12,3 +12,3 @@ export const normalizeBodyMethods = (body) => { | ||
destroy: () => {}, | ||
} | ||
}; | ||
} | ||
@@ -20,5 +20,5 @@ | ||
destroy: () => { | ||
body.close() | ||
body.close(); | ||
}, | ||
} | ||
}; | ||
} | ||
@@ -30,5 +30,5 @@ | ||
destroy: () => { | ||
body.destroy() | ||
body.destroy(); | ||
}, | ||
} | ||
}; | ||
} | ||
@@ -39,8 +39,8 @@ | ||
destroy: () => {}, | ||
} | ||
} | ||
}; | ||
}; | ||
export const isFileHandle = (value) => { | ||
return value && value.constructor && value.constructor.name === "FileHandle" | ||
} | ||
return value && value.constructor && value.constructor.name === "FileHandle"; | ||
}; | ||
@@ -58,3 +58,3 @@ export const fileHandleToReadableStream = (fileHandle) => { | ||
}, | ||
) | ||
); | ||
// I suppose it's required only when doing fs.createReadStream() | ||
@@ -65,12 +65,12 @@ // and not fileHandle.createReadStream() | ||
// }) | ||
return fileReadableStream | ||
} | ||
return fileReadableStream; | ||
}; | ||
const fileHandleToObservable = (fileHandle) => { | ||
return observableFromNodeStream(fileHandleToReadableStream(fileHandle)) | ||
} | ||
return observableFromNodeStream(fileHandleToReadableStream(fileHandle)); | ||
}; | ||
const isNodeStream = (value) => { | ||
if (value === undefined) { | ||
return false | ||
return false; | ||
} | ||
@@ -83,6 +83,6 @@ | ||
) { | ||
return true | ||
return true; | ||
} | ||
return false | ||
} | ||
return false; | ||
}; |
// https://github.com/jamestalmage/stream-to-observable/blob/master/index.js | ||
import { Readable } from "node:stream" | ||
import { createObservable } from "./observable.js" | ||
import { Readable } from "node:stream"; | ||
import { createObservable } from "./observable.js"; | ||
@@ -13,27 +13,27 @@ export const observableFromNodeStream = ( | ||
if (nodeStream.isPaused()) { | ||
nodeStream.resume() | ||
nodeStream.resume(); | ||
} else if (nodeStream.complete) { | ||
complete() | ||
return null | ||
complete(); | ||
return null; | ||
} | ||
const cleanup = () => { | ||
nodeStream.removeListener("data", next) | ||
nodeStream.removeListener("error", error) | ||
nodeStream.removeListener("end", complete) | ||
nodeStream.removeListener("close", cleanup) | ||
nodeStream.destroy() | ||
} | ||
nodeStream.removeListener("data", next); | ||
nodeStream.removeListener("error", error); | ||
nodeStream.removeListener("end", complete); | ||
nodeStream.removeListener("close", cleanup); | ||
nodeStream.destroy(); | ||
}; | ||
// should we do nodeStream.resume() in case the stream was paused ? | ||
nodeStream.once("error", error) | ||
nodeStream.once("error", error); | ||
nodeStream.on("data", (data) => { | ||
next(data) | ||
}) | ||
next(data); | ||
}); | ||
nodeStream.once("close", () => { | ||
cleanup() | ||
}) | ||
cleanup(); | ||
}); | ||
nodeStream.once("end", () => { | ||
complete() | ||
}) | ||
return cleanup | ||
}) | ||
complete(); | ||
}); | ||
return cleanup; | ||
}); | ||
@@ -53,22 +53,22 @@ if (nodeStream instanceof Readable) { | ||
}, | ||
) | ||
nodeStream.destroy() | ||
}, readableStreamLifetime) | ||
observable.timeout = timeout | ||
); | ||
nodeStream.destroy(); | ||
}, readableStreamLifetime); | ||
observable.timeout = timeout; | ||
onceReadableStreamUsedOrClosed(nodeStream, () => { | ||
clearTimeout(timeout) | ||
}) | ||
clearTimeout(timeout); | ||
}); | ||
} | ||
return observable | ||
} | ||
return observable; | ||
}; | ||
const onceReadableStreamUsedOrClosed = (readableStream, callback) => { | ||
const dataOrCloseCallback = () => { | ||
readableStream.removeListener("data", dataOrCloseCallback) | ||
readableStream.removeListener("close", dataOrCloseCallback) | ||
callback() | ||
} | ||
readableStream.on("data", dataOrCloseCallback) | ||
readableStream.once("close", dataOrCloseCallback) | ||
} | ||
readableStream.removeListener("data", dataOrCloseCallback); | ||
readableStream.removeListener("close", dataOrCloseCallback); | ||
callback(); | ||
}; | ||
readableStream.on("data", dataOrCloseCallback); | ||
readableStream.once("close", dataOrCloseCallback); | ||
}; |
if ("observable" in Symbol === false) { | ||
Symbol.observable = Symbol.for("observable") | ||
Symbol.observable = Symbol.for("observable"); | ||
} | ||
@@ -7,3 +7,3 @@ | ||
if (typeof producer !== "function") { | ||
throw new TypeError(`producer must be a function, got ${producer}`) | ||
throw new TypeError(`producer must be a function, got ${producer}`); | ||
} | ||
@@ -16,94 +16,94 @@ | ||
error = (value) => { | ||
throw value | ||
throw value; | ||
}, | ||
complete = () => {}, | ||
}) => { | ||
let cleanup = () => {} | ||
let cleanup = () => {}; | ||
const subscription = { | ||
closed: false, | ||
unsubscribe: () => { | ||
subscription.closed = true | ||
cleanup() | ||
subscription.closed = true; | ||
cleanup(); | ||
}, | ||
} | ||
}; | ||
const producerReturnValue = producer({ | ||
next: (value) => { | ||
if (subscription.closed) return | ||
next(value) | ||
if (subscription.closed) return; | ||
next(value); | ||
}, | ||
error: (value) => { | ||
if (subscription.closed) return | ||
error(value) | ||
if (subscription.closed) return; | ||
error(value); | ||
}, | ||
complete: () => { | ||
if (subscription.closed) return | ||
complete() | ||
if (subscription.closed) return; | ||
complete(); | ||
}, | ||
}) | ||
}); | ||
if (typeof producerReturnValue === "function") { | ||
cleanup = producerReturnValue | ||
cleanup = producerReturnValue; | ||
} | ||
return subscription | ||
return subscription; | ||
}, | ||
} | ||
}; | ||
return observable | ||
} | ||
return observable; | ||
}; | ||
export const isObservable = (value) => { | ||
if (value === null || value === undefined) { | ||
return false | ||
return false; | ||
} | ||
if (typeof value === "object" || typeof value === "function") { | ||
return Symbol.observable in value | ||
return Symbol.observable in value; | ||
} | ||
return false | ||
} | ||
return false; | ||
}; | ||
export const observableFromValue = (value) => { | ||
if (isObservable(value)) { | ||
return value | ||
return value; | ||
} | ||
return createObservable(({ next, complete }) => { | ||
next(value) | ||
next(value); | ||
const timer = setTimeout(() => { | ||
complete() | ||
}) | ||
complete(); | ||
}); | ||
return () => { | ||
clearTimeout(timer) | ||
} | ||
}) | ||
} | ||
clearTimeout(timer); | ||
}; | ||
}); | ||
}; | ||
export const createCompositeProducer = ({ cleanup = () => {} } = {}) => { | ||
const observables = new Set() | ||
const observers = new Set() | ||
const observables = new Set(); | ||
const observers = new Set(); | ||
const addObservable = (observable) => { | ||
if (observables.has(observable)) { | ||
return false | ||
return false; | ||
} | ||
observables.add(observable) | ||
observables.add(observable); | ||
observers.forEach((observer) => { | ||
observer.observe(observable) | ||
}) | ||
return true | ||
} | ||
observer.observe(observable); | ||
}); | ||
return true; | ||
}; | ||
const removeObservable = (observable) => { | ||
if (!observables.has(observable)) { | ||
return false | ||
return false; | ||
} | ||
observables.delete(observable) | ||
observables.delete(observable); | ||
observers.forEach((observer) => { | ||
observer.unobserve(observable) | ||
}) | ||
return true | ||
} | ||
observer.unobserve(observable); | ||
}); | ||
return true; | ||
}; | ||
@@ -115,60 +115,60 @@ const producer = ({ | ||
}) => { | ||
let completeCount = 0 | ||
let completeCount = 0; | ||
const checkComplete = () => { | ||
if (completeCount === observables.size) { | ||
complete() | ||
complete(); | ||
} | ||
} | ||
}; | ||
const subscriptions = new Map() | ||
const subscriptions = new Map(); | ||
const observe = (observable) => { | ||
const subscription = observable.subscribe({ | ||
next: (value) => { | ||
next(value) | ||
next(value); | ||
}, | ||
error: (value) => { | ||
error(value) | ||
error(value); | ||
}, | ||
complete: () => { | ||
subscriptions.delete(observable) | ||
completeCount++ | ||
checkComplete() | ||
subscriptions.delete(observable); | ||
completeCount++; | ||
checkComplete(); | ||
}, | ||
}) | ||
subscriptions.set(observable, subscription) | ||
} | ||
}); | ||
subscriptions.set(observable, subscription); | ||
}; | ||
const unobserve = (observable) => { | ||
const subscription = subscriptions.get(observable) | ||
const subscription = subscriptions.get(observable); | ||
if (!subscription) { | ||
return | ||
return; | ||
} | ||
subscription.unsubscribe() | ||
subscriptions.delete(observable) | ||
checkComplete() | ||
} | ||
subscription.unsubscribe(); | ||
subscriptions.delete(observable); | ||
checkComplete(); | ||
}; | ||
const observer = { | ||
observe, | ||
unobserve, | ||
} | ||
observers.add(observer) | ||
}; | ||
observers.add(observer); | ||
observables.forEach((observable) => { | ||
observe(observable) | ||
}) | ||
observe(observable); | ||
}); | ||
return () => { | ||
observers.delete(observer) | ||
observers.delete(observer); | ||
subscriptions.forEach((subscription) => { | ||
subscription.unsubscribe() | ||
}) | ||
subscriptions.clear() | ||
cleanup() | ||
} | ||
} | ||
subscription.unsubscribe(); | ||
}); | ||
subscriptions.clear(); | ||
cleanup(); | ||
}; | ||
}; | ||
producer.addObservable = addObservable | ||
producer.removeObservable = removeObservable | ||
producer.addObservable = addObservable; | ||
producer.removeObservable = removeObservable; | ||
return producer | ||
} | ||
return producer; | ||
}; |
@@ -1,3 +0,3 @@ | ||
import { observableFromNodeStream } from "./observable_from_node_stream.js" | ||
import { headersFromObject } from "../internal/headersFromObject.js" | ||
import { observableFromNodeStream } from "./observable_from_node_stream.js"; | ||
import { headersFromObject } from "../internal/headersFromObject.js"; | ||
@@ -8,20 +8,20 @@ export const fromNodeRequest = ( | ||
) => { | ||
const headers = headersFromObject(nodeRequest.headers) | ||
const headers = headersFromObject(nodeRequest.headers); | ||
const body = observableFromNodeStream(nodeRequest, { | ||
readableStreamLifetime: requestBodyLifetime, | ||
}) | ||
}); | ||
let requestOrigin | ||
let requestOrigin; | ||
if (nodeRequest.upgrade) { | ||
requestOrigin = serverOrigin | ||
requestOrigin = serverOrigin; | ||
} else if (nodeRequest.authority) { | ||
requestOrigin = nodeRequest.connection.encrypted | ||
? `https://${nodeRequest.authority}` | ||
: `http://${nodeRequest.authority}` | ||
: `http://${nodeRequest.authority}`; | ||
} else if (nodeRequest.headers.host) { | ||
requestOrigin = nodeRequest.connection.encrypted | ||
? `https://${nodeRequest.headers.host}` | ||
: `http://${nodeRequest.headers.host}` | ||
: `http://${nodeRequest.headers.host}`; | ||
} else { | ||
requestOrigin = serverOrigin | ||
requestOrigin = serverOrigin; | ||
} | ||
@@ -40,4 +40,4 @@ | ||
body, | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -62,8 +62,8 @@ export const applyRedirectionToRequest = ( | ||
...rest, | ||
} | ||
} | ||
}; | ||
}; | ||
const getPropertiesFromResource = ({ resource, baseUrl }) => { | ||
const urlObject = new URL(resource, baseUrl) | ||
let pathname = urlObject.pathname | ||
const urlObject = new URL(resource, baseUrl); | ||
let pathname = urlObject.pathname; | ||
@@ -74,4 +74,4 @@ return { | ||
resource, | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -82,4 +82,4 @@ const getPropertiesFromPathname = ({ pathname, baseUrl }) => { | ||
baseUrl, | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -101,8 +101,8 @@ export const createPushRequest = (request, { signal, pathname, method }) => { | ||
body: undefined, | ||
}) | ||
return pushRequest | ||
} | ||
}); | ||
return pushRequest; | ||
}; | ||
const getHeadersInheritedByPushRequest = (request) => { | ||
const headersInherited = { ...request.headers } | ||
const headersInherited = { ...request.headers }; | ||
// mtime sent by the client in request headers concerns the main request | ||
@@ -115,5 +115,5 @@ // Time remains valid for request to other resources so we keep it | ||
// A request made to an other resource must not inherit the eTag | ||
delete headersInherited["if-none-match"] | ||
delete headersInherited["if-none-match"]; | ||
return headersInherited | ||
} | ||
return headersInherited; | ||
}; |
@@ -1,6 +0,6 @@ | ||
import http from "node:http" | ||
import { Http2ServerResponse } from "node:http2" | ||
import { raceCallbacks } from "@jsenv/abort" | ||
import http from "node:http"; | ||
import { Http2ServerResponse } from "node:http2"; | ||
import { raceCallbacks } from "@jsenv/abort"; | ||
import { normalizeBodyMethods } from "./body.js" | ||
import { normalizeBodyMethods } from "./body.js"; | ||
@@ -12,10 +12,10 @@ export const writeNodeResponse = async ( | ||
) => { | ||
body = await body | ||
const bodyMethods = normalizeBodyMethods(body) | ||
body = await body; | ||
const bodyMethods = normalizeBodyMethods(body); | ||
if (signal.aborted) { | ||
bodyMethods.destroy() | ||
responseStream.destroy() | ||
onAbort() | ||
return | ||
bodyMethods.destroy(); | ||
responseStream.destroy(); | ||
onAbort(); | ||
return; | ||
} | ||
@@ -28,27 +28,27 @@ | ||
onHeadersSent, | ||
}) | ||
}); | ||
if (!body) { | ||
onEnd() | ||
responseStream.end() | ||
return | ||
onEnd(); | ||
responseStream.end(); | ||
return; | ||
} | ||
if (ignoreBody) { | ||
onEnd() | ||
bodyMethods.destroy() | ||
responseStream.end() | ||
return | ||
onEnd(); | ||
bodyMethods.destroy(); | ||
responseStream.end(); | ||
return; | ||
} | ||
if (bodyEncoding) { | ||
responseStream.setEncoding(bodyEncoding) | ||
responseStream.setEncoding(bodyEncoding); | ||
} | ||
await new Promise((resolve) => { | ||
const observable = bodyMethods.asObservable() | ||
const observable = bodyMethods.asObservable(); | ||
const subscription = observable.subscribe({ | ||
next: (data) => { | ||
try { | ||
responseStream.write(data) | ||
responseStream.write(data); | ||
} catch (e) { | ||
@@ -64,14 +64,14 @@ // Something inside Node.js sometimes puts stream | ||
if (e.code === "ERR_HTTP2_INVALID_STREAM") { | ||
return | ||
return; | ||
} | ||
responseStream.emit("error", e) | ||
responseStream.emit("error", e); | ||
} | ||
}, | ||
error: (value) => { | ||
responseStream.emit("error", value) | ||
responseStream.emit("error", value); | ||
}, | ||
complete: () => { | ||
responseStream.end() | ||
responseStream.end(); | ||
}, | ||
}) | ||
}); | ||
@@ -81,24 +81,24 @@ raceCallbacks( | ||
abort: (cb) => { | ||
signal.addEventListener("abort", cb) | ||
signal.addEventListener("abort", cb); | ||
return () => { | ||
signal.removeEventListener("abort", cb) | ||
} | ||
signal.removeEventListener("abort", cb); | ||
}; | ||
}, | ||
error: (cb) => { | ||
responseStream.on("error", cb) | ||
responseStream.on("error", cb); | ||
return () => { | ||
responseStream.removeListener("error", cb) | ||
} | ||
responseStream.removeListener("error", cb); | ||
}; | ||
}, | ||
close: (cb) => { | ||
responseStream.on("close", cb) | ||
responseStream.on("close", cb); | ||
return () => { | ||
responseStream.removeListener("close", cb) | ||
} | ||
responseStream.removeListener("close", cb); | ||
}; | ||
}, | ||
finish: (cb) => { | ||
responseStream.on("finish", cb) | ||
responseStream.on("finish", cb); | ||
return () => { | ||
responseStream.removeListener("finish", cb) | ||
} | ||
responseStream.removeListener("finish", cb); | ||
}; | ||
}, | ||
@@ -109,12 +109,12 @@ }, | ||
abort: () => { | ||
subscription.unsubscribe() | ||
responseStream.destroy() | ||
onAbort() | ||
resolve() | ||
subscription.unsubscribe(); | ||
responseStream.destroy(); | ||
onAbort(); | ||
resolve(); | ||
}, | ||
error: (error) => { | ||
subscription.unsubscribe() | ||
responseStream.destroy() | ||
onError(error) | ||
resolve() | ||
subscription.unsubscribe(); | ||
responseStream.destroy(); | ||
onError(error); | ||
resolve(); | ||
}, | ||
@@ -127,17 +127,17 @@ close: () => { | ||
// and the browser is reloaded or closed for instance | ||
subscription.unsubscribe() | ||
responseStream.destroy() | ||
onAbort() | ||
resolve() | ||
subscription.unsubscribe(); | ||
responseStream.destroy(); | ||
onAbort(); | ||
resolve(); | ||
}, | ||
finish: () => { | ||
onEnd() | ||
resolve() | ||
onEnd(); | ||
resolve(); | ||
}, | ||
} | ||
raceEffects[winner.name](winner.data) | ||
}; | ||
raceEffects[winner.name](winner.data); | ||
}, | ||
) | ||
}) | ||
} | ||
); | ||
}); | ||
}; | ||
@@ -149,5 +149,5 @@ const writeHead = ( | ||
const responseIsHttp2ServerResponse = | ||
responseStream instanceof Http2ServerResponse | ||
responseStream instanceof Http2ServerResponse; | ||
const responseIsServerHttp2Stream = | ||
responseStream.constructor.name === "ServerHttp2Stream" | ||
responseStream.constructor.name === "ServerHttp2Stream"; | ||
let nodeHeaders = headersToNodeHeaders(headers, { | ||
@@ -157,5 +157,5 @@ // https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L112 | ||
responseIsHttp2ServerResponse || responseIsServerHttp2Stream, | ||
}) | ||
}); | ||
if (statusText === undefined) { | ||
statusText = statusTextFromStatus(status) | ||
statusText = statusTextFromStatus(status); | ||
} | ||
@@ -166,6 +166,6 @@ if (responseIsServerHttp2Stream) { | ||
":status": status, | ||
} | ||
responseStream.respond(nodeHeaders) | ||
onHeadersSent({ nodeHeaders, status, statusText }) | ||
return | ||
}; | ||
responseStream.respond(nodeHeaders); | ||
onHeadersSent({ nodeHeaders, status, statusText }); | ||
return; | ||
} | ||
@@ -178,9 +178,9 @@ // nodejs strange signature for writeHead force this | ||
) { | ||
responseStream.writeHead(status, nodeHeaders) | ||
onHeadersSent({ nodeHeaders, status, statusText }) | ||
return | ||
responseStream.writeHead(status, nodeHeaders); | ||
onHeadersSent({ nodeHeaders, status, statusText }); | ||
return; | ||
} | ||
try { | ||
responseStream.writeHead(status, statusText, nodeHeaders) | ||
responseStream.writeHead(status, statusText, nodeHeaders); | ||
} catch (e) { | ||
@@ -193,23 +193,23 @@ if ( | ||
--- status message --- | ||
${statusText}`) | ||
${statusText}`); | ||
} | ||
throw e | ||
throw e; | ||
} | ||
onHeadersSent({ nodeHeaders, status, statusText }) | ||
} | ||
onHeadersSent({ nodeHeaders, status, statusText }); | ||
}; | ||
const statusTextFromStatus = (status) => | ||
http.STATUS_CODES[status] || "not specified" | ||
http.STATUS_CODES[status] || "not specified"; | ||
const headersToNodeHeaders = (headers, { ignoreConnectionHeader }) => { | ||
const nodeHeaders = {} | ||
const nodeHeaders = {}; | ||
Object.keys(headers).forEach((name) => { | ||
if (name === "connection" && ignoreConnectionHeader) return | ||
const nodeHeaderName = name in mapping ? mapping[name] : name | ||
nodeHeaders[nodeHeaderName] = headers[name] | ||
}) | ||
if (name === "connection" && ignoreConnectionHeader) return; | ||
const nodeHeaderName = name in mapping ? mapping[name] : name; | ||
nodeHeaders[nodeHeaderName] = headers[name]; | ||
}); | ||
return nodeHeaders | ||
} | ||
return nodeHeaders; | ||
}; | ||
@@ -219,2 +219,2 @@ const mapping = { | ||
// "last-modified": "Last-Modified", | ||
} | ||
}; |
// https://github.com/Marak/colors.js/blob/b63ef88e521b42920a9e908848de340b31e68c9d/lib/styles.js#L29 | ||
const close = "\x1b[0m" | ||
const red = "\x1b[31m" | ||
const green = "\x1b[32m" | ||
const yellow = "\x1b[33m" | ||
const close = "\x1b[0m"; | ||
const red = "\x1b[31m"; | ||
const green = "\x1b[32m"; | ||
const yellow = "\x1b[33m"; | ||
// const blue = "\x1b[34m" | ||
const magenta = "\x1b[35m" | ||
const cyan = "\x1b[36m" | ||
const magenta = "\x1b[35m"; | ||
const cyan = "\x1b[36m"; | ||
// const white = "\x1b[37m" | ||
export const colorizeResponseStatus = (status) => { | ||
const statusType = statusToType(status) | ||
if (statusType === "information") return `${cyan}${status}${close}` | ||
if (statusType === "success") return `${green}${status}${close}` | ||
if (statusType === "redirection") return `${magenta}${status}${close}` | ||
if (statusType === "client_error") return `${yellow}${status}${close}` | ||
if (statusType === "server_error") return `${red}${status}${close}` | ||
return status | ||
} | ||
const statusType = statusToType(status); | ||
if (statusType === "information") return `${cyan}${status}${close}`; | ||
if (statusType === "success") return `${green}${status}${close}`; | ||
if (statusType === "redirection") return `${magenta}${status}${close}`; | ||
if (statusType === "client_error") return `${yellow}${status}${close}`; | ||
if (statusType === "server_error") return `${red}${status}${close}`; | ||
return status; | ||
}; | ||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status | ||
export const statusToType = (status) => { | ||
if (statusIsInformation(status)) return "information" | ||
if (statusIsSuccess(status)) return "success" | ||
if (statusIsRedirection(status)) return "redirection" | ||
if (statusIsClientError(status)) return "client_error" | ||
if (statusIsServerError(status)) return "server_error" | ||
return "unknown" | ||
} | ||
if (statusIsInformation(status)) return "information"; | ||
if (statusIsSuccess(status)) return "success"; | ||
if (statusIsRedirection(status)) return "redirection"; | ||
if (statusIsClientError(status)) return "client_error"; | ||
if (statusIsServerError(status)) return "server_error"; | ||
return "unknown"; | ||
}; | ||
const statusIsInformation = (status) => status >= 100 && status < 200 | ||
const statusIsInformation = (status) => status >= 100 && status < 200; | ||
const statusIsSuccess = (status) => status >= 200 && status < 300 | ||
const statusIsSuccess = (status) => status >= 200 && status < 300; | ||
const statusIsRedirection = (status) => status >= 300 && status < 400 | ||
const statusIsRedirection = (status) => status >= 300 && status < 400; | ||
const statusIsClientError = (status) => status >= 400 && status < 500 | ||
const statusIsClientError = (status) => status >= 400 && status < 500; | ||
const statusIsServerError = (status) => status >= 500 && status < 600 | ||
const statusIsServerError = (status) => status >= 500 && status < 600; |
@@ -7,3 +7,3 @@ export const convertFileSystemErrorToResponseProperties = (error) => { | ||
statusText: `EACCES: No permission to read file at ${error.path}`, | ||
} | ||
}; | ||
} | ||
@@ -14,3 +14,3 @@ if (isErrorWithCode(error, "EPERM")) { | ||
statusText: `EPERM: No permission to read file at ${error.path}`, | ||
} | ||
}; | ||
} | ||
@@ -21,3 +21,3 @@ if (isErrorWithCode(error, "ENOENT")) { | ||
statusText: `ENOENT: File not found at ${error.path}`, | ||
} | ||
}; | ||
} | ||
@@ -33,3 +33,3 @@ // file access may be temporarily blocked | ||
}, | ||
} | ||
}; | ||
} | ||
@@ -44,3 +44,3 @@ // emfile means there is too many files currently opened | ||
}, | ||
} | ||
}; | ||
} | ||
@@ -51,9 +51,9 @@ if (isErrorWithCode(error, "EISDIR")) { | ||
statusText: `EISDIR: Unexpected directory operation at ${error.path}`, | ||
} | ||
}; | ||
} | ||
return null | ||
} | ||
return null; | ||
}; | ||
const isErrorWithCode = (error, code) => { | ||
return typeof error === "object" && error.code === code | ||
} | ||
return typeof error === "object" && error.code === code; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { lookup } from "node:dns" | ||
import { lookup } from "node:dns"; | ||
@@ -10,9 +10,9 @@ export const applyDnsResolution = async ( | ||
if (error) { | ||
reject(error) | ||
reject(error); | ||
} else { | ||
resolve({ address, family }) | ||
resolve({ address, family }); | ||
} | ||
}) | ||
}) | ||
return dnsResolution | ||
} | ||
}); | ||
}); | ||
return dnsResolution; | ||
}; |
@@ -1,22 +0,22 @@ | ||
import { createHash } from "node:crypto" | ||
import { createHash } from "node:crypto"; | ||
const ETAG_FOR_EMPTY_CONTENT = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"' | ||
const ETAG_FOR_EMPTY_CONTENT = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'; | ||
export const bufferToEtag = (buffer) => { | ||
if (!Buffer.isBuffer(buffer)) { | ||
throw new TypeError(`buffer expected, got ${buffer}`) | ||
throw new TypeError(`buffer expected, got ${buffer}`); | ||
} | ||
if (buffer.length === 0) { | ||
return ETAG_FOR_EMPTY_CONTENT | ||
return ETAG_FOR_EMPTY_CONTENT; | ||
} | ||
const hash = createHash("sha1") | ||
hash.update(buffer, "utf8") | ||
const hash = createHash("sha1"); | ||
hash.update(buffer, "utf8"); | ||
const hashBase64String = hash.digest("base64") | ||
const hashBase64StringSubset = hashBase64String.slice(0, 27) | ||
const length = buffer.length | ||
const hashBase64String = hash.digest("base64"); | ||
const hashBase64StringSubset = hashBase64String.slice(0, 27); | ||
const length = buffer.length; | ||
return `"${length.toString(16)}-${hashBase64StringSubset}"` | ||
} | ||
return `"${length.toString(16)}-${hashBase64StringSubset}"`; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { pathToFileURL, fileURLToPath } from "node:url" | ||
import { pathToFileURL, fileURLToPath } from "node:url"; | ||
@@ -7,38 +7,38 @@ export const isFileSystemPath = (value) => { | ||
`isFileSystemPath first arg must be a string, got ${value}`, | ||
) | ||
); | ||
} | ||
if (value[0] === "/") { | ||
return true | ||
return true; | ||
} | ||
return startsWithWindowsDriveLetter(value) | ||
} | ||
return startsWithWindowsDriveLetter(value); | ||
}; | ||
const startsWithWindowsDriveLetter = (string) => { | ||
const firstChar = string[0] | ||
if (!/[a-zA-Z]/.test(firstChar)) return false | ||
const firstChar = string[0]; | ||
if (!/[a-zA-Z]/.test(firstChar)) return false; | ||
const secondChar = string[1] | ||
if (secondChar !== ":") return false | ||
const secondChar = string[1]; | ||
if (secondChar !== ":") return false; | ||
return true | ||
} | ||
return true; | ||
}; | ||
export const fileSystemPathToUrl = (value) => { | ||
if (!isFileSystemPath(value)) { | ||
throw new Error(`received an invalid value for fileSystemPath: ${value}`) | ||
throw new Error(`received an invalid value for fileSystemPath: ${value}`); | ||
} | ||
return String(pathToFileURL(value)) | ||
} | ||
return String(pathToFileURL(value)); | ||
}; | ||
export const urlToFileSystemPath = (url) => { | ||
let urlString = String(url) | ||
let urlString = String(url); | ||
if (urlString[urlString.length - 1] === "/") { | ||
// remove trailing / so that nodejs path becomes predictable otherwise it logs | ||
// the trailing slash on linux but does not on windows | ||
urlString = urlString.slice(0, -1) | ||
urlString = urlString.slice(0, -1); | ||
} | ||
const fileSystemPath = fileURLToPath(urlString) | ||
return fileSystemPath | ||
} | ||
const fileSystemPath = fileURLToPath(urlString); | ||
return fileSystemPath; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { composeTwoObjects } from "./object_composition.js" | ||
import { composeTwoObjects } from "./object_composition.js"; | ||
@@ -7,14 +7,14 @@ export const composeTwoHeaders = (firstHeaders, secondHeaders) => { | ||
forceLowerCase: true, | ||
}) | ||
} | ||
}); | ||
}; | ||
const composeHeaderValues = (value, nextValue) => { | ||
const headerValues = value.split(", ") | ||
const headerValues = value.split(", "); | ||
nextValue.split(", ").forEach((value) => { | ||
if (!headerValues.includes(value)) { | ||
headerValues.push(value) | ||
headerValues.push(value); | ||
} | ||
}) | ||
return headerValues.join(", ") | ||
} | ||
}); | ||
return headerValues.join(", "); | ||
}; | ||
@@ -33,2 +33,2 @@ const HEADER_NAMES_COMPOSITION = { | ||
"vary": composeHeaderValues, | ||
} | ||
}; |
@@ -6,7 +6,7 @@ /* | ||
import { normalizeHeaderName } from "./normalizeHeaderName.js" | ||
import { normalizeHeaderValue } from "./normalizeHeaderValue.js" | ||
import { normalizeHeaderName } from "./normalizeHeaderName.js"; | ||
import { normalizeHeaderValue } from "./normalizeHeaderValue.js"; | ||
export const headersFromObject = (headersObject) => { | ||
const headers = {} | ||
const headers = {}; | ||
@@ -16,10 +16,10 @@ Object.keys(headersObject).forEach((headerName) => { | ||
// exclude http2 headers | ||
return | ||
return; | ||
} | ||
headers[normalizeHeaderName(headerName)] = normalizeHeaderValue( | ||
headersObject[headerName], | ||
) | ||
}) | ||
); | ||
}); | ||
return headers | ||
} | ||
return headers; | ||
}; |
@@ -1,21 +0,21 @@ | ||
import { normalizeHeaderName } from "./normalizeHeaderName.js" | ||
import { normalizeHeaderValue } from "./normalizeHeaderValue.js" | ||
import { normalizeHeaderName } from "./normalizeHeaderName.js"; | ||
import { normalizeHeaderValue } from "./normalizeHeaderValue.js"; | ||
// https://gist.github.com/mmazer/5404301 | ||
export const headersFromString = (headerString) => { | ||
const headers = {} | ||
const headers = {}; | ||
if (headerString) { | ||
const pairs = headerString.split("\r\n") | ||
const pairs = headerString.split("\r\n"); | ||
pairs.forEach((pair) => { | ||
const index = pair.indexOf(": ") | ||
const index = pair.indexOf(": "); | ||
if (index > 0) { | ||
const key = pair.slice(0, index) | ||
const value = pair.slice(index + 2) | ||
headers[normalizeHeaderName(key)] = normalizeHeaderValue(value) | ||
const key = pair.slice(0, index); | ||
const value = pair.slice(index + 2); | ||
headers[normalizeHeaderName(key)] = normalizeHeaderValue(value); | ||
} | ||
}) | ||
}); | ||
} | ||
return headers | ||
} | ||
return headers; | ||
}; |
export const headersToObject = (headers) => { | ||
const headersObject = {} | ||
const headersObject = {}; | ||
headers.forEach((value, name) => { | ||
headersObject[name] = value | ||
}) | ||
return headersObject | ||
} | ||
headersObject[name] = value; | ||
}); | ||
return headersObject; | ||
}; |
export const headersToString = (headers, { convertName = (name) => name }) => { | ||
const headersString = headersToArray(headers).map(({ name, value }) => { | ||
return `${convertName(name)}: ${value}` | ||
}) | ||
return `${convertName(name)}: ${value}`; | ||
}); | ||
return headersString.join("\r\n") | ||
} | ||
return headersString.join("\r\n"); | ||
}; | ||
@@ -14,4 +14,4 @@ const headersToArray = (headers) => { | ||
value: headers[name], | ||
} | ||
}) | ||
} | ||
}; | ||
}); | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { isIP } from "node:net" | ||
import { isIP } from "node:net"; | ||
@@ -9,3 +9,3 @@ export const parseHostname = (hostname) => { | ||
version: 4, | ||
} | ||
}; | ||
} | ||
@@ -20,3 +20,3 @@ if ( | ||
version: 6, | ||
} | ||
}; | ||
} | ||
@@ -28,3 +28,3 @@ if (hostname === "127.0.0.1") { | ||
version: 4, | ||
} | ||
}; | ||
} | ||
@@ -39,9 +39,9 @@ if ( | ||
version: 6, | ||
} | ||
}; | ||
} | ||
const ipVersion = isIP(hostname) | ||
const ipVersion = isIP(hostname); | ||
if (ipVersion === 0) { | ||
return { | ||
type: "hostname", | ||
} | ||
}; | ||
} | ||
@@ -51,3 +51,3 @@ return { | ||
version: ipVersion, | ||
} | ||
} | ||
}; | ||
}; |
@@ -1,3 +0,3 @@ | ||
import { createServer } from "node:net" | ||
import { Abort } from "@jsenv/abort" | ||
import { createServer } from "node:net"; | ||
import { Abort } from "@jsenv/abort"; | ||
@@ -11,24 +11,24 @@ const listen = async ({ | ||
}) => { | ||
const listeningOperation = Abort.startOperation() | ||
const listeningOperation = Abort.startOperation(); | ||
try { | ||
listeningOperation.addAbortSignal(signal) | ||
listeningOperation.addAbortSignal(signal); | ||
if (portHint) { | ||
listeningOperation.throwIfAborted() | ||
listeningOperation.throwIfAborted(); | ||
port = await findFreePort(portHint, { | ||
signal: listeningOperation.signal, | ||
hostname, | ||
}) | ||
}); | ||
} | ||
listeningOperation.throwIfAborted() | ||
port = await startListening({ server, port, hostname }) | ||
listeningOperation.addAbortCallback(() => stopListening(server)) | ||
listeningOperation.throwIfAborted() | ||
listeningOperation.throwIfAborted(); | ||
port = await startListening({ server, port, hostname }); | ||
listeningOperation.addAbortCallback(() => stopListening(server)); | ||
listeningOperation.throwIfAborted(); | ||
return port | ||
return port; | ||
} finally { | ||
await listeningOperation.end() | ||
await listeningOperation.end(); | ||
} | ||
} | ||
}; | ||
@@ -45,31 +45,31 @@ export const findFreePort = async ( | ||
) => { | ||
const findFreePortOperation = Abort.startOperation() | ||
const findFreePortOperation = Abort.startOperation(); | ||
try { | ||
findFreePortOperation.addAbortSignal(signal) | ||
findFreePortOperation.throwIfAborted() | ||
findFreePortOperation.addAbortSignal(signal); | ||
findFreePortOperation.throwIfAborted(); | ||
const testUntil = async (port, host) => { | ||
findFreePortOperation.throwIfAborted() | ||
const free = await portIsFree(port, host) | ||
findFreePortOperation.throwIfAborted(); | ||
const free = await portIsFree(port, host); | ||
if (free) { | ||
return port | ||
return port; | ||
} | ||
const nextPort = next(port) | ||
const nextPort = next(port); | ||
if (nextPort > max) { | ||
throw new Error( | ||
`${hostname} has no available port between ${min} and ${max}`, | ||
) | ||
); | ||
} | ||
return testUntil(nextPort, hostname) | ||
} | ||
const freePort = await testUntil(initialPort, hostname) | ||
return freePort | ||
return testUntil(nextPort, hostname); | ||
}; | ||
const freePort = await testUntil(initialPort, hostname); | ||
return freePort; | ||
} finally { | ||
await findFreePortOperation.end() | ||
await findFreePortOperation.end(); | ||
} | ||
} | ||
}; | ||
const portIsFree = async (port, hostname) => { | ||
const server = createServer() | ||
const server = createServer(); | ||
@@ -81,38 +81,38 @@ try { | ||
hostname, | ||
}) | ||
}); | ||
} catch (error) { | ||
if (error && error.code === "EADDRINUSE") { | ||
return false | ||
return false; | ||
} | ||
if (error && error.code === "EACCES") { | ||
return false | ||
return false; | ||
} | ||
throw error | ||
throw error; | ||
} | ||
await stopListening(server) | ||
return true | ||
} | ||
await stopListening(server); | ||
return true; | ||
}; | ||
const startListening = ({ server, port, hostname }) => { | ||
return new Promise((resolve, reject) => { | ||
server.on("error", reject) | ||
server.on("error", reject); | ||
server.on("listening", () => { | ||
// in case port is 0 (randomly assign an available port) | ||
// https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback | ||
resolve(server.address().port) | ||
}) | ||
server.listen(port, hostname) | ||
}) | ||
} | ||
resolve(server.address().port); | ||
}); | ||
server.listen(port, hostname); | ||
}); | ||
}; | ||
export const stopListening = (server) => { | ||
return new Promise((resolve, reject) => { | ||
server.on("error", reject) | ||
server.on("close", resolve) | ||
server.close() | ||
}) | ||
} | ||
server.on("error", reject); | ||
server.on("close", resolve); | ||
server.close(); | ||
}); | ||
}; | ||
// unit test exports | ||
export { listen, portIsFree } | ||
export { listen, portIsFree }; |
@@ -1,2 +0,2 @@ | ||
import { listenEvent } from "./listenEvent.js" | ||
import { listenEvent } from "./listenEvent.js"; | ||
@@ -9,3 +9,3 @@ export const listenClientError = (nodeServer, clientErrorCallback) => { | ||
clientErrorCallback, | ||
) | ||
); | ||
const removeHttpClientError = listenEvent( | ||
@@ -15,3 +15,3 @@ nodeServer._httpServer, | ||
clientErrorCallback, | ||
) | ||
); | ||
const removeTlsClientError = listenEvent( | ||
@@ -21,10 +21,10 @@ nodeServer._tlsServer, | ||
clientErrorCallback, | ||
) | ||
); | ||
return () => { | ||
removeNetClientError() | ||
removeHttpClientError() | ||
removeTlsClientError() | ||
} | ||
removeNetClientError(); | ||
removeHttpClientError(); | ||
removeTlsClientError(); | ||
}; | ||
} | ||
return listenEvent(nodeServer, "clientError", clientErrorCallback) | ||
} | ||
return listenEvent(nodeServer, "clientError", clientErrorCallback); | ||
}; |
@@ -8,9 +8,9 @@ export const listenEvent = ( | ||
if (once) { | ||
objectWithEventEmitter.once(eventName, callback) | ||
objectWithEventEmitter.once(eventName, callback); | ||
} else { | ||
objectWithEventEmitter.addListener(eventName, callback) | ||
objectWithEventEmitter.addListener(eventName, callback); | ||
} | ||
return () => { | ||
objectWithEventEmitter.removeListener(eventName, callback) | ||
} | ||
} | ||
objectWithEventEmitter.removeListener(eventName, callback); | ||
}; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { listenEvent } from "./listenEvent.js" | ||
import { listenEvent } from "./listenEvent.js"; | ||
@@ -9,3 +9,3 @@ export const listenRequest = (nodeServer, requestCallback) => { | ||
requestCallback, | ||
) | ||
); | ||
const removeTlsRequestListener = listenEvent( | ||
@@ -15,9 +15,9 @@ nodeServer._tlsServer, | ||
requestCallback, | ||
) | ||
); | ||
return () => { | ||
removeHttpRequestListener() | ||
removeTlsRequestListener() | ||
} | ||
removeHttpRequestListener(); | ||
removeTlsRequestListener(); | ||
}; | ||
} | ||
return listenEvent(nodeServer, "request", requestCallback) | ||
} | ||
return listenEvent(nodeServer, "request", requestCallback); | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { listenEvent } from "./listenEvent.js" | ||
import { listenEvent } from "./listenEvent.js"; | ||
@@ -8,3 +8,3 @@ export const listenServerConnectionError = ( | ||
) => { | ||
const cleanupSet = new Set() | ||
const cleanupSet = new Set(); | ||
@@ -20,7 +20,7 @@ const removeConnectionListener = listenEvent( | ||
if (ignoreErrorAfterConnectionIsDestroyed && socket.destroyed) { | ||
return | ||
return; | ||
} | ||
connectionErrorCallback(error, socket) | ||
connectionErrorCallback(error, socket); | ||
}, | ||
) | ||
); | ||
const removeOnceSocketCloseListener = listenEvent( | ||
@@ -30,4 +30,4 @@ socket, | ||
() => { | ||
removeSocketErrorListener() | ||
cleanupSet.delete(cleanup) | ||
removeSocketErrorListener(); | ||
cleanupSet.delete(cleanup); | ||
}, | ||
@@ -37,17 +37,17 @@ { | ||
}, | ||
) | ||
); | ||
const cleanup = () => { | ||
removeSocketErrorListener() | ||
removeOnceSocketCloseListener() | ||
} | ||
cleanupSet.add(cleanup) | ||
removeSocketErrorListener(); | ||
removeOnceSocketCloseListener(); | ||
}; | ||
cleanupSet.add(cleanup); | ||
}, | ||
) | ||
); | ||
return () => { | ||
removeConnectionListener() | ||
removeConnectionListener(); | ||
cleanupSet.forEach((cleanup) => { | ||
cleanup() | ||
}) | ||
cleanupSet.clear() | ||
} | ||
} | ||
cleanup(); | ||
}); | ||
cleanupSet.clear(); | ||
}; | ||
}; |
@@ -18,11 +18,11 @@ /** | ||
) => { | ||
const values = multipleHeaderString.split(",") | ||
const multipleHeader = {} | ||
const values = multipleHeaderString.split(","); | ||
const multipleHeader = {}; | ||
values.forEach((value) => { | ||
const valueTrimmed = value.trim() | ||
const valueParts = valueTrimmed.split(";") | ||
const name = valueParts[0] | ||
const nameValidation = validateName(name) | ||
const valueTrimmed = value.trim(); | ||
const valueParts = valueTrimmed.split(";"); | ||
const name = valueParts[0]; | ||
const nameValidation = validateName(name); | ||
if (!nameValidation) { | ||
return | ||
return; | ||
} | ||
@@ -32,16 +32,16 @@ | ||
validateProperty, | ||
}) | ||
multipleHeader[name] = properties | ||
}) | ||
return multipleHeader | ||
} | ||
}); | ||
multipleHeader[name] = properties; | ||
}); | ||
return multipleHeader; | ||
}; | ||
const parseHeaderProperties = (headerProperties, { validateProperty }) => { | ||
const properties = headerProperties.reduce((previous, valuePart) => { | ||
const [propertyName, propertyValueString] = valuePart.split("=") | ||
const propertyValue = parseHeaderPropertyValue(propertyValueString) | ||
const property = { name: propertyName, value: propertyValue } | ||
const propertyValidation = validateProperty(property) | ||
const [propertyName, propertyValueString] = valuePart.split("="); | ||
const propertyValue = parseHeaderPropertyValue(propertyValueString); | ||
const property = { name: propertyName, value: propertyValue }; | ||
const propertyValidation = validateProperty(property); | ||
if (!propertyValidation) { | ||
return previous | ||
return previous; | ||
} | ||
@@ -51,19 +51,19 @@ return { | ||
[property.name]: property.value, | ||
} | ||
}, {}) | ||
return properties | ||
} | ||
}; | ||
}, {}); | ||
return properties; | ||
}; | ||
const parseHeaderPropertyValue = (headerPropertyValueString) => { | ||
const firstChar = headerPropertyValueString[0] | ||
const firstChar = headerPropertyValueString[0]; | ||
const lastChar = | ||
headerPropertyValueString[headerPropertyValueString.length - 1] | ||
headerPropertyValueString[headerPropertyValueString.length - 1]; | ||
if (firstChar === '"' && lastChar === '"') { | ||
return headerPropertyValueString.slice(1, -1) | ||
return headerPropertyValueString.slice(1, -1); | ||
} | ||
if (isNaN(headerPropertyValueString)) { | ||
return headerPropertyValueString | ||
return headerPropertyValueString; | ||
} | ||
return parseFloat(headerPropertyValueString) | ||
} | ||
return parseFloat(headerPropertyValueString); | ||
}; | ||
@@ -76,17 +76,17 @@ export const stringifyMultipleHeader = ( | ||
.filter((name) => { | ||
const headerProperties = multipleHeader[name] | ||
const headerProperties = multipleHeader[name]; | ||
if (!headerProperties) { | ||
return false | ||
return false; | ||
} | ||
if (typeof headerProperties !== "object") { | ||
return false | ||
return false; | ||
} | ||
const nameValidation = validateName(name) | ||
const nameValidation = validateName(name); | ||
if (!nameValidation) { | ||
return false | ||
return false; | ||
} | ||
return true | ||
return true; | ||
}) | ||
.map((name) => { | ||
const headerProperties = multipleHeader[name] | ||
const headerProperties = multipleHeader[name]; | ||
const headerPropertiesString = stringifyHeaderProperties( | ||
@@ -97,10 +97,10 @@ headerProperties, | ||
}, | ||
) | ||
); | ||
if (headerPropertiesString.length) { | ||
return `${name};${headerPropertiesString}` | ||
return `${name};${headerPropertiesString}`; | ||
} | ||
return name | ||
return name; | ||
}) | ||
.join(", ") | ||
} | ||
.join(", "); | ||
}; | ||
@@ -113,22 +113,22 @@ const stringifyHeaderProperties = (headerProperties, { validateProperty }) => { | ||
value: headerProperties[name], | ||
} | ||
return property | ||
}; | ||
return property; | ||
}) | ||
.filter((property) => { | ||
const propertyValidation = validateProperty(property) | ||
const propertyValidation = validateProperty(property); | ||
if (!propertyValidation) { | ||
return false | ||
return false; | ||
} | ||
return true | ||
return true; | ||
}) | ||
.map(stringifyHeaderProperty) | ||
.join(";") | ||
return headerPropertiesString | ||
} | ||
.join(";"); | ||
return headerPropertiesString; | ||
}; | ||
const stringifyHeaderProperty = ({ name, value }) => { | ||
if (typeof value === "string") { | ||
return `${name}="${value}"` | ||
return `${name}="${value}"`; | ||
} | ||
return `${name}=${value}` | ||
} | ||
return `${name}=${value}`; | ||
}; |
export const normalizeHeaderName = (headerName) => { | ||
headerName = String(headerName) | ||
headerName = String(headerName); | ||
if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(headerName)) { | ||
throw new TypeError("Invalid character in header field name") | ||
throw new TypeError("Invalid character in header field name"); | ||
} | ||
return headerName.toLowerCase() | ||
} | ||
return headerName.toLowerCase(); | ||
}; |
export const normalizeHeaderValue = (headerValue) => { | ||
return String(headerValue) | ||
} | ||
return String(headerValue); | ||
}; |
@@ -10,3 +10,3 @@ export const composeTwoObjects = ( | ||
strict, | ||
}) | ||
}); | ||
} | ||
@@ -17,4 +17,4 @@ | ||
strict, | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -27,3 +27,3 @@ const applyCaseSensitiveComposition = ( | ||
if (strict) { | ||
const composed = {} | ||
const composed = {}; | ||
Object.keys(keysComposition).forEach((key) => { | ||
@@ -37,11 +37,11 @@ composed[key] = composeValueAtKey({ | ||
secondKey: keyExistsIn(key, secondObject) ? key : null, | ||
}) | ||
}) | ||
return composed | ||
}); | ||
}); | ||
return composed; | ||
} | ||
const composed = {} | ||
const composed = {}; | ||
Object.keys(firstObject).forEach((key) => { | ||
composed[key] = firstObject[key] | ||
}) | ||
composed[key] = firstObject[key]; | ||
}); | ||
Object.keys(secondObject).forEach((key) => { | ||
@@ -55,6 +55,6 @@ composed[key] = composeValueAtKey({ | ||
secondKey: keyExistsIn(key, secondObject) ? key : null, | ||
}) | ||
}) | ||
return composed | ||
} | ||
}); | ||
}); | ||
return composed; | ||
}; | ||
@@ -67,10 +67,10 @@ const applyCompositionForcingLowerCase = ( | ||
if (strict) { | ||
const firstObjectKeyMapping = {} | ||
const firstObjectKeyMapping = {}; | ||
Object.keys(firstObject).forEach((key) => { | ||
firstObjectKeyMapping[key.toLowerCase()] = key | ||
}) | ||
const secondObjectKeyMapping = {} | ||
firstObjectKeyMapping[key.toLowerCase()] = key; | ||
}); | ||
const secondObjectKeyMapping = {}; | ||
Object.keys(secondObject).forEach((key) => { | ||
secondObjectKeyMapping[key.toLowerCase()] = key | ||
}) | ||
secondObjectKeyMapping[key.toLowerCase()] = key; | ||
}); | ||
Object.keys(keysComposition).forEach((key) => { | ||
@@ -84,12 +84,12 @@ composed[key] = composeValueAtKey({ | ||
secondKey: secondObjectKeyMapping[key] || null, | ||
}) | ||
}) | ||
}); | ||
}); | ||
} | ||
const composed = {} | ||
const composed = {}; | ||
Object.keys(firstObject).forEach((key) => { | ||
composed[key.toLowerCase()] = firstObject[key] | ||
}) | ||
composed[key.toLowerCase()] = firstObject[key]; | ||
}); | ||
Object.keys(secondObject).forEach((key) => { | ||
const keyLowercased = key.toLowerCase() | ||
const keyLowercased = key.toLowerCase(); | ||
@@ -111,6 +111,6 @@ composed[key.toLowerCase()] = composeValueAtKey({ | ||
: null, | ||
}) | ||
}) | ||
return composed | ||
} | ||
}); | ||
}); | ||
return composed; | ||
}; | ||
@@ -126,20 +126,22 @@ const composeValueAtKey = ({ | ||
if (!firstKey) { | ||
return secondObject[secondKey] | ||
return secondObject[secondKey]; | ||
} | ||
if (!secondKey) { | ||
return firstObject[firstKey] | ||
return firstObject[firstKey]; | ||
} | ||
const keyForCustomComposition = keyExistsIn(key, keysComposition) ? key : null | ||
const keyForCustomComposition = keyExistsIn(key, keysComposition) | ||
? key | ||
: null; | ||
if (!keyForCustomComposition) { | ||
return secondObject[secondKey] | ||
return secondObject[secondKey]; | ||
} | ||
const composeTwoValues = keysComposition[keyForCustomComposition] | ||
return composeTwoValues(firstObject[firstKey], secondObject[secondKey]) | ||
} | ||
const composeTwoValues = keysComposition[keyForCustomComposition]; | ||
return composeTwoValues(firstObject[firstKey], secondObject[secondKey]); | ||
}; | ||
const keyExistsIn = (key, object) => { | ||
return Object.prototype.hasOwnProperty.call(object, key) | ||
} | ||
return Object.prototype.hasOwnProperty.call(object, key); | ||
}; |
@@ -1,3 +0,3 @@ | ||
import { composeTwoObjects } from "./object_composition.js" | ||
import { composeTwoHeaders } from "./headers_composition.js" | ||
import { composeTwoObjects } from "./object_composition.js"; | ||
import { composeTwoHeaders } from "./headers_composition.js"; | ||
@@ -8,4 +8,4 @@ export const composeTwoResponses = (firstResponse, secondResponse) => { | ||
strict: true, | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -20,4 +20,4 @@ const RESPONSE_KEYS_COMPOSITION = { | ||
timing: (prevTiming, timing) => { | ||
return { ...prevTiming, ...timing } | ||
return { ...prevTiming, ...timing }; | ||
}, | ||
} | ||
}; |
@@ -1,9 +0,9 @@ | ||
import { networkInterfaces } from "node:os" | ||
import { networkInterfaces } from "node:os"; | ||
export const createIpGetters = () => { | ||
const networkAddresses = [] | ||
const networkInterfaceMap = networkInterfaces() | ||
const networkAddresses = []; | ||
const networkInterfaceMap = networkInterfaces(); | ||
for (const key of Object.keys(networkInterfaceMap)) { | ||
for (const networkAddress of networkInterfaceMap[key]) { | ||
networkAddresses.push(networkAddress) | ||
networkAddresses.push(networkAddress); | ||
} | ||
@@ -13,29 +13,29 @@ } | ||
getFirstInternalIp: ({ preferIpv6 }) => { | ||
const isPref = preferIpv6 ? isIpV6 : isIpV4 | ||
let firstInternalIp | ||
const isPref = preferIpv6 ? isIpV6 : isIpV4; | ||
let firstInternalIp; | ||
for (const networkAddress of networkAddresses) { | ||
if (networkAddress.internal) { | ||
firstInternalIp = networkAddress.address | ||
firstInternalIp = networkAddress.address; | ||
if (isPref(networkAddress)) { | ||
break | ||
break; | ||
} | ||
} | ||
} | ||
return firstInternalIp | ||
return firstInternalIp; | ||
}, | ||
getFirstExternalIp: ({ preferIpv6 }) => { | ||
const isPref = preferIpv6 ? isIpV6 : isIpV4 | ||
let firstExternalIp | ||
const isPref = preferIpv6 ? isIpV6 : isIpV4; | ||
let firstExternalIp; | ||
for (const networkAddress of networkAddresses) { | ||
if (!networkAddress.internal) { | ||
firstExternalIp = networkAddress.address | ||
firstExternalIp = networkAddress.address; | ||
if (isPref(networkAddress)) { | ||
break | ||
break; | ||
} | ||
} | ||
} | ||
return firstExternalIp | ||
return firstExternalIp; | ||
}, | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -45,7 +45,7 @@ const isIpV4 = (networkAddress) => { | ||
if (typeof networkAddress.family === "number") { | ||
return networkAddress.family === 4 | ||
return networkAddress.family === 4; | ||
} | ||
return networkAddress.family === "IPv4" | ||
} | ||
return networkAddress.family === "IPv4"; | ||
}; | ||
const isIpV6 = (networkAddress) => !isIpV4(networkAddress) | ||
const isIpV6 = (networkAddress) => !isIpV4(networkAddress); |
@@ -7,5 +7,5 @@ /** | ||
import http from "node:http" | ||
import net from "node:net" | ||
import { listenEvent } from "./listenEvent.js" | ||
import http from "node:http"; | ||
import net from "node:net"; | ||
import { listenEvent } from "./listenEvent.js"; | ||
@@ -18,3 +18,3 @@ export const createPolyglotServer = async ({ | ||
}) => { | ||
const httpServer = http.createServer() | ||
const httpServer = http.createServer(); | ||
const tlsServer = await createSecureServer({ | ||
@@ -25,6 +25,6 @@ certificate, | ||
http1Allowed, | ||
}) | ||
}); | ||
const netServer = net.createServer({ | ||
allowHalfOpen: false, | ||
}) | ||
}); | ||
@@ -34,9 +34,9 @@ listenEvent(netServer, "connection", (socket) => { | ||
if (protocol === "http") { | ||
httpServer.emit("connection", socket) | ||
return | ||
httpServer.emit("connection", socket); | ||
return; | ||
} | ||
if (protocol === "tls") { | ||
tlsServer.emit("connection", socket) | ||
return | ||
tlsServer.emit("connection", socket); | ||
return; | ||
} | ||
@@ -49,6 +49,6 @@ | ||
"", | ||
].join("\r\n") | ||
socket.write(response) | ||
socket.end() | ||
socket.destroy() | ||
].join("\r\n"); | ||
socket.write(response); | ||
socket.end(); | ||
socket.destroy(); | ||
netServer.emit( | ||
@@ -58,11 +58,11 @@ "clientError", | ||
socket, | ||
) | ||
}) | ||
}) | ||
); | ||
}); | ||
}); | ||
netServer._httpServer = httpServer | ||
netServer._tlsServer = tlsServer | ||
netServer._httpServer = httpServer; | ||
netServer._tlsServer = tlsServer; | ||
return netServer | ||
} | ||
return netServer; | ||
}; | ||
@@ -79,3 +79,3 @@ // The async part is just to lazyly import "http2" or "https" | ||
if (http2) { | ||
const { createSecureServer } = await import("node:http2") | ||
const { createSecureServer } = await import("node:http2"); | ||
return createSecureServer({ | ||
@@ -85,40 +85,40 @@ cert: certificate, | ||
allowHTTP1: http1Allowed, | ||
}) | ||
}); | ||
} | ||
const { createServer } = await import("node:https") | ||
const { createServer } = await import("node:https"); | ||
return createServer({ | ||
cert: certificate, | ||
key: privateKey, | ||
}) | ||
} | ||
}); | ||
}; | ||
const detectSocketProtocol = (socket, protocolDetectedCallback) => { | ||
let removeOnceReadableListener = () => {} | ||
let removeOnceReadableListener = () => {}; | ||
const tryToRead = () => { | ||
const buffer = socket.read(1) | ||
const buffer = socket.read(1); | ||
if (buffer === null) { | ||
removeOnceReadableListener = socket.once("readable", tryToRead) | ||
return | ||
removeOnceReadableListener = socket.once("readable", tryToRead); | ||
return; | ||
} | ||
const firstByte = buffer[0] | ||
socket.unshift(buffer) | ||
const firstByte = buffer[0]; | ||
socket.unshift(buffer); | ||
if (firstByte === 22) { | ||
protocolDetectedCallback("tls") | ||
return | ||
protocolDetectedCallback("tls"); | ||
return; | ||
} | ||
if (firstByte > 32 && firstByte < 127) { | ||
protocolDetectedCallback("http") | ||
return | ||
protocolDetectedCallback("http"); | ||
return; | ||
} | ||
protocolDetectedCallback(null) | ||
} | ||
protocolDetectedCallback(null); | ||
}; | ||
tryToRead() | ||
tryToRead(); | ||
return () => { | ||
removeOnceReadableListener() | ||
} | ||
} | ||
removeOnceReadableListener(); | ||
}; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { listenEvent } from "./listenEvent.js" | ||
import { listenEvent } from "./listenEvent.js"; | ||
@@ -6,6 +6,6 @@ export const trackServerPendingConnections = (nodeServer, { http2 }) => { | ||
// see http2.js: we rely on https://nodejs.org/api/http2.html#http2_compatibility_api | ||
return trackHttp1ServerPendingConnections(nodeServer) | ||
return trackHttp1ServerPendingConnections(nodeServer); | ||
} | ||
return trackHttp1ServerPendingConnections(nodeServer) | ||
} | ||
return trackHttp1ServerPendingConnections(nodeServer); | ||
}; | ||
@@ -15,3 +15,3 @@ // const trackHttp2ServerPendingSessions = () => {} | ||
const trackHttp1ServerPendingConnections = (nodeServer) => { | ||
const pendingConnections = new Set() | ||
const pendingConnections = new Set(); | ||
@@ -22,3 +22,3 @@ const removeConnectionListener = listenEvent( | ||
(connection) => { | ||
pendingConnections.add(connection) | ||
pendingConnections.add(connection); | ||
listenEvent( | ||
@@ -28,23 +28,23 @@ connection, | ||
() => { | ||
pendingConnections.delete(connection) | ||
pendingConnections.delete(connection); | ||
}, | ||
{ once: true }, | ||
) | ||
); | ||
}, | ||
) | ||
); | ||
const stop = async (reason) => { | ||
removeConnectionListener() | ||
const pendingConnectionsArray = Array.from(pendingConnections) | ||
pendingConnections.clear() | ||
removeConnectionListener(); | ||
const pendingConnectionsArray = Array.from(pendingConnections); | ||
pendingConnections.clear(); | ||
await Promise.all( | ||
pendingConnectionsArray.map(async (pendingConnection) => { | ||
await destroyConnection(pendingConnection, reason) | ||
await destroyConnection(pendingConnection, reason); | ||
}), | ||
) | ||
} | ||
); | ||
}; | ||
return { stop } | ||
} | ||
return { stop }; | ||
}; | ||
@@ -56,12 +56,12 @@ const destroyConnection = (connection, reason) => { | ||
if (error === reason || error.code === "ENOTCONN") { | ||
resolve() | ||
resolve(); | ||
} else { | ||
reject(error) | ||
reject(error); | ||
} | ||
} else { | ||
resolve() | ||
resolve(); | ||
} | ||
}) | ||
}) | ||
} | ||
}); | ||
}); | ||
}; | ||
@@ -68,0 +68,0 @@ // export const trackServerPendingStreams = (nodeServer) => { |
@@ -1,2 +0,2 @@ | ||
import { listenRequest } from "./listenRequest.js" | ||
import { listenRequest } from "./listenRequest.js"; | ||
@@ -6,9 +6,9 @@ export const trackServerPendingRequests = (nodeServer, { http2 }) => { | ||
// see http2.js: we rely on https://nodejs.org/api/http2.html#http2_compatibility_api | ||
return trackHttp1ServerPendingRequests(nodeServer) | ||
return trackHttp1ServerPendingRequests(nodeServer); | ||
} | ||
return trackHttp1ServerPendingRequests(nodeServer) | ||
} | ||
return trackHttp1ServerPendingRequests(nodeServer); | ||
}; | ||
const trackHttp1ServerPendingRequests = (nodeServer) => { | ||
const pendingClients = new Set() | ||
const pendingClients = new Set(); | ||
@@ -18,18 +18,18 @@ const removeRequestListener = listenRequest( | ||
(nodeRequest, nodeResponse) => { | ||
const client = { nodeRequest, nodeResponse } | ||
pendingClients.add(client) | ||
const client = { nodeRequest, nodeResponse }; | ||
pendingClients.add(client); | ||
nodeResponse.once("close", () => { | ||
pendingClients.delete(client) | ||
}) | ||
pendingClients.delete(client); | ||
}); | ||
}, | ||
) | ||
); | ||
const stop = async ({ status, reason }) => { | ||
removeRequestListener() | ||
const pendingClientsArray = Array.from(pendingClients) | ||
pendingClients.clear() | ||
removeRequestListener(); | ||
const pendingClientsArray = Array.from(pendingClients); | ||
pendingClients.clear(); | ||
await Promise.all( | ||
pendingClientsArray.map(({ nodeResponse }) => { | ||
if (nodeResponse.headersSent === false) { | ||
nodeResponse.writeHead(status, String(reason)) | ||
nodeResponse.writeHead(status, String(reason)); | ||
} | ||
@@ -41,13 +41,13 @@ | ||
if (nodeResponse.closed) { | ||
resolve() | ||
resolve(); | ||
} else { | ||
nodeResponse.close((error) => { | ||
if (error) { | ||
reject(error) | ||
reject(error); | ||
} else { | ||
resolve() | ||
resolve(); | ||
} | ||
}) | ||
}); | ||
} | ||
}) | ||
}); | ||
} | ||
@@ -58,15 +58,15 @@ | ||
if (nodeResponse.destroyed) { | ||
resolve() | ||
resolve(); | ||
} else { | ||
nodeResponse.once("close", () => { | ||
resolve() | ||
}) | ||
nodeResponse.destroy() | ||
resolve(); | ||
}); | ||
nodeResponse.destroy(); | ||
} | ||
}) | ||
}); | ||
}), | ||
) | ||
} | ||
); | ||
}; | ||
return { stop } | ||
} | ||
return { stop }; | ||
}; |
@@ -7,6 +7,6 @@ /* | ||
export { startServer } from "./start_server.js" | ||
export { setupRoutes } from "./service_composition/routing.js" | ||
export { readRequestBody } from "./readRequestBody.js" | ||
export { fetchFileSystem } from "./fetch_filesystem.js" | ||
export { startServer } from "./start_server.js"; | ||
export { setupRoutes } from "./service_composition/routing.js"; | ||
export { readRequestBody } from "./readRequestBody.js"; | ||
export { fetchFileSystem } from "./fetch_filesystem.js"; | ||
export { | ||
@@ -20,4 +20,4 @@ STOP_REASON_INTERNAL_ERROR, | ||
STOP_REASON_NOT_SPECIFIED, | ||
} from "./stopReasons.js" | ||
export { jsenvServiceErrorHandler } from "./services/error_handler/jsenv_service_error_handler.js" | ||
} from "./stopReasons.js"; | ||
export { jsenvServiceErrorHandler } from "./services/error_handler/jsenv_service_error_handler.js"; | ||
@@ -29,21 +29,21 @@ // CORS | ||
jsenvAccessControlAllowedMethods, | ||
} from "./services/cors/jsenv_service_cors.js" | ||
} from "./services/cors/jsenv_service_cors.js"; | ||
// server timing | ||
export { timeFunction, timeStart } from "./server_timing/timing_measure.js" | ||
export { timeFunction, timeStart } from "./server_timing/timing_measure.js"; | ||
// SSE | ||
export { createSSERoom } from "./sse/sse_room.js" | ||
export { createSSERoom } from "./sse/sse_room.js"; | ||
// content-negotiation | ||
export { pickContentType } from "./content_negotiation/pick_content_type.js" | ||
export { pickContentEncoding } from "./content_negotiation/pick_content_encoding.js" | ||
export { pickContentLanguage } from "./content_negotiation/pick_content_language.js" | ||
export { jsenvServiceResponseAcceptanceCheck } from "./services/response_acceptance_check/jsenv_service_response_acceptance_check.js" | ||
export { pickContentType } from "./content_negotiation/pick_content_type.js"; | ||
export { pickContentEncoding } from "./content_negotiation/pick_content_encoding.js"; | ||
export { pickContentLanguage } from "./content_negotiation/pick_content_language.js"; | ||
export { jsenvServiceResponseAcceptanceCheck } from "./services/response_acceptance_check/jsenv_service_response_acceptance_check.js"; | ||
// others | ||
export { serveDirectory } from "./serve_directory.js" | ||
export { fromFetchResponse } from "./from_fetch_response.js" | ||
export { composeTwoResponses } from "./internal/response_composition.js" | ||
export { jsenvServiceRequestAliases } from "./services/request_aliases/jsenv_service_request_aliases.js" | ||
export { findFreePort } from "./internal/listen.js" | ||
export { serveDirectory } from "./serve_directory.js"; | ||
export { fromFetchResponse } from "./from_fetch_response.js"; | ||
export { composeTwoResponses } from "./internal/response_composition.js"; | ||
export { jsenvServiceRequestAliases } from "./services/request_aliases/jsenv_service_request_aliases.js"; | ||
export { findFreePort } from "./internal/listen.js"; |
export const readRequestBody = (request, { as = "string" } = {}) => { | ||
return new Promise((resolve, reject) => { | ||
const bufferArray = [] | ||
const bufferArray = []; | ||
request.body.subscribe({ | ||
error: reject, | ||
next: (buffer) => { | ||
bufferArray.push(buffer) | ||
bufferArray.push(buffer); | ||
}, | ||
complete: () => { | ||
const bodyAsBuffer = Buffer.concat(bufferArray) | ||
const bodyAsBuffer = Buffer.concat(bufferArray); | ||
if (as === "buffer") { | ||
resolve(bodyAsBuffer) | ||
return | ||
resolve(bodyAsBuffer); | ||
return; | ||
} | ||
if (as === "string") { | ||
const bodyAsString = bodyAsBuffer.toString() | ||
resolve(bodyAsString) | ||
return | ||
const bodyAsString = bodyAsBuffer.toString(); | ||
resolve(bodyAsString); | ||
return; | ||
} | ||
if (as === "json") { | ||
const bodyAsString = bodyAsBuffer.toString() | ||
const bodyAsJSON = JSON.parse(bodyAsString) | ||
resolve(bodyAsJSON) | ||
return | ||
const bodyAsString = bodyAsBuffer.toString(); | ||
const bodyAsJSON = JSON.parse(bodyAsString); | ||
resolve(bodyAsJSON); | ||
return; | ||
} | ||
}, | ||
}) | ||
}) | ||
} | ||
}); | ||
}); | ||
}; |
@@ -1,4 +0,4 @@ | ||
import { readdirSync } from "node:fs" | ||
import { readdirSync } from "node:fs"; | ||
import { pickContentType } from "./content_negotiation/pick_content_type.js" | ||
import { pickContentType } from "./content_negotiation/pick_content_type.js"; | ||
@@ -9,8 +9,8 @@ export const serveDirectory = ( | ||
) => { | ||
url = String(url) | ||
url = url[url.length - 1] === "/" ? url : `${url}/` | ||
const directoryContentArray = readdirSync(new URL(url)) | ||
url = String(url); | ||
url = url[url.length - 1] === "/" ? url : `${url}/`; | ||
const directoryContentArray = readdirSync(new URL(url)); | ||
const responseProducers = { | ||
"application/json": () => { | ||
const directoryContentJson = JSON.stringify(directoryContentArray) | ||
const directoryContentJson = JSON.stringify(directoryContentArray); | ||
return { | ||
@@ -23,3 +23,3 @@ status: 200, | ||
body: directoryContentJson, | ||
} | ||
}; | ||
}, | ||
@@ -39,9 +39,9 @@ "text/html": () => { | ||
${directoryContentArray.map((filename) => { | ||
const fileUrl = String(new URL(filename, url)) | ||
const fileUrl = String(new URL(filename, url)); | ||
const fileUrlRelativeToServer = fileUrl.slice( | ||
String(rootDirectoryUrl).length, | ||
) | ||
); | ||
return `<li> | ||
<a href="/${fileUrlRelativeToServer}">${fileUrlRelativeToServer}</a> | ||
</li>` | ||
</li>`; | ||
}).join(` | ||
@@ -51,3 +51,3 @@ `)} | ||
</body> | ||
</html>` | ||
</html>`; | ||
@@ -61,10 +61,10 @@ return { | ||
body: directoryAsHtml, | ||
} | ||
}; | ||
}, | ||
} | ||
}; | ||
const bestContentType = pickContentType( | ||
{ headers }, | ||
Object.keys(responseProducers), | ||
) | ||
return responseProducers[bestContentType || "application/json"]() | ||
} | ||
); | ||
return responseProducers[bestContentType || "application/json"](); | ||
}; |
import { | ||
parseMultipleHeader, | ||
stringifyMultipleHeader, | ||
} from "../internal/multiple-header.js" | ||
} from "../internal/multiple-header.js"; | ||
@@ -13,15 +13,15 @@ // to predict order in chrome devtools we should put a,b,c,d,e or something | ||
export const timingToServerTimingResponseHeaders = (timing) => { | ||
const serverTimingHeader = {} | ||
const serverTimingHeader = {}; | ||
Object.keys(timing).forEach((key, index) => { | ||
const name = letters[index] || "zz" | ||
const name = letters[index] || "zz"; | ||
serverTimingHeader[name] = { | ||
desc: key, | ||
dur: timing[key], | ||
} | ||
}) | ||
}; | ||
}); | ||
const serverTimingHeaderString = | ||
stringifyServerTimingHeader(serverTimingHeader) | ||
stringifyServerTimingHeader(serverTimingHeader); | ||
return { "server-timing": serverTimingHeaderString } | ||
} | ||
return { "server-timing": serverTimingHeaderString }; | ||
}; | ||
@@ -34,17 +34,17 @@ export const parseServerTimingHeader = (serverTimingHeaderString) => { | ||
validateProperty: ({ name }) => { | ||
return name === "desc" || name === "dur" | ||
return name === "desc" || name === "dur"; | ||
}, | ||
}, | ||
) | ||
); | ||
const serverTiming = {} | ||
const serverTiming = {}; | ||
Object.keys(serverTimingHeaderObject).forEach((key) => { | ||
const { desc, dur } = serverTimingHeaderObject[key] | ||
const { desc, dur } = serverTimingHeaderObject[key]; | ||
serverTiming[key] = { | ||
...(desc ? { description: desc } : {}), | ||
...(dur ? { duration: dur } : {}), | ||
} | ||
}) | ||
return serverTiming | ||
} | ||
}; | ||
}); | ||
return serverTiming; | ||
}; | ||
@@ -54,4 +54,4 @@ export const stringifyServerTimingHeader = (serverTimingHeader) => { | ||
validateName: validateServerTimingName, | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -65,9 +65,9 @@ // (),/:;<=>?@[\]{}" Don't allowed | ||
const validateServerTimingName = (name) => { | ||
const valid = /^[!#$%&'*+\-.^_`|~0-9a-z]+$/gi.test(name) | ||
const valid = /^[!#$%&'*+\-.^_`|~0-9a-z]+$/gi.test(name); | ||
if (!valid) { | ||
console.warn(`server timing contains invalid symbols`) | ||
return false | ||
console.warn(`server timing contains invalid symbols`); | ||
return false; | ||
} | ||
return true | ||
} | ||
return true; | ||
}; | ||
@@ -101,2 +101,2 @@ const letters = [ | ||
"z", | ||
] | ||
]; |
@@ -1,2 +0,2 @@ | ||
import { performance } from "node:perf_hooks" | ||
import { performance } from "node:perf_hooks"; | ||
@@ -6,22 +6,22 @@ export const timeStart = (name) => { | ||
// duration is a https://www.w3.org/TR/hr-time-2/#sec-domhighrestimestamp | ||
const startTimestamp = performance.now() | ||
const startTimestamp = performance.now(); | ||
const timeEnd = () => { | ||
const endTimestamp = performance.now() | ||
const endTimestamp = performance.now(); | ||
const timing = { | ||
[name]: endTimestamp - startTimestamp, | ||
} | ||
return timing | ||
} | ||
return timeEnd | ||
} | ||
}; | ||
return timing; | ||
}; | ||
return timeEnd; | ||
}; | ||
export const timeFunction = (name, fn) => { | ||
const timeEnd = timeStart(name) | ||
const returnValue = fn() | ||
const timeEnd = timeStart(name); | ||
const returnValue = fn(); | ||
if (returnValue && typeof returnValue.then === "function") { | ||
return returnValue.then((value) => { | ||
return [timeEnd(), value] | ||
}) | ||
return [timeEnd(), value]; | ||
}); | ||
} | ||
return [timeEnd(), returnValue] | ||
} | ||
return [timeEnd(), returnValue]; | ||
}; |
@@ -8,27 +8,27 @@ // ESM version of "path-to-regexp@6.2.1" | ||
function lexer(str) { | ||
var tokens = [] | ||
var i = 0 | ||
var tokens = []; | ||
var i = 0; | ||
while (i < str.length) { | ||
var char = str[i] | ||
var char = str[i]; | ||
if (char === "*" || char === "+" || char === "?") { | ||
tokens.push({ type: "MODIFIER", index: i, value: str[i++] }) | ||
continue | ||
tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); | ||
continue; | ||
} | ||
if (char === "\\") { | ||
tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }) | ||
continue | ||
tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); | ||
continue; | ||
} | ||
if (char === "{") { | ||
tokens.push({ type: "OPEN", index: i, value: str[i++] }) | ||
continue | ||
tokens.push({ type: "OPEN", index: i, value: str[i++] }); | ||
continue; | ||
} | ||
if (char === "}") { | ||
tokens.push({ type: "CLOSE", index: i, value: str[i++] }) | ||
continue | ||
tokens.push({ type: "CLOSE", index: i, value: str[i++] }); | ||
continue; | ||
} | ||
if (char === ":") { | ||
var name = "" | ||
let j = i + 1 | ||
var name = ""; | ||
let j = i + 1; | ||
while (j < str.length) { | ||
var code = str.charCodeAt(j) | ||
var code = str.charCodeAt(j); | ||
if ( | ||
@@ -44,50 +44,50 @@ // `0-9` | ||
) { | ||
name += str[j++] | ||
continue | ||
name += str[j++]; | ||
continue; | ||
} | ||
break | ||
break; | ||
} | ||
if (!name) throw new TypeError("Missing parameter name at ".concat(i)) | ||
tokens.push({ type: "NAME", index: i, value: name }) | ||
i = j | ||
continue | ||
if (!name) throw new TypeError("Missing parameter name at ".concat(i)); | ||
tokens.push({ type: "NAME", index: i, value: name }); | ||
i = j; | ||
continue; | ||
} | ||
if (char === "(") { | ||
var count = 1 | ||
var pattern = "" | ||
var j = i + 1 | ||
var count = 1; | ||
var pattern = ""; | ||
var j = i + 1; | ||
if (str[j] === "?") { | ||
throw new TypeError('Pattern cannot start with "?" at '.concat(j)) | ||
throw new TypeError('Pattern cannot start with "?" at '.concat(j)); | ||
} | ||
while (j < str.length) { | ||
if (str[j] === "\\") { | ||
pattern += str[j++] + str[j++] | ||
continue | ||
pattern += str[j++] + str[j++]; | ||
continue; | ||
} | ||
if (str[j] === ")") { | ||
count-- | ||
count--; | ||
if (count === 0) { | ||
j++ | ||
break | ||
j++; | ||
break; | ||
} | ||
} else if (str[j] === "(") { | ||
count++ | ||
count++; | ||
if (str[j + 1] !== "?") { | ||
throw new TypeError( | ||
"Capturing groups are not allowed at ".concat(j), | ||
) | ||
); | ||
} | ||
} | ||
pattern += str[j++] | ||
pattern += str[j++]; | ||
} | ||
if (count) throw new TypeError("Unbalanced pattern at ".concat(i)) | ||
if (!pattern) throw new TypeError("Missing pattern at ".concat(i)) | ||
tokens.push({ type: "PATTERN", index: i, value: pattern }) | ||
i = j | ||
continue | ||
if (count) throw new TypeError("Unbalanced pattern at ".concat(i)); | ||
if (!pattern) throw new TypeError("Missing pattern at ".concat(i)); | ||
tokens.push({ type: "PATTERN", index: i, value: pattern }); | ||
i = j; | ||
continue; | ||
} | ||
tokens.push({ type: "CHAR", index: i, value: str[i++] }) | ||
tokens.push({ type: "CHAR", index: i, value: str[i++] }); | ||
} | ||
tokens.push({ type: "END", index: i, value: "" }) | ||
return tokens | ||
tokens.push({ type: "END", index: i, value: "" }); | ||
return tokens; | ||
} | ||
@@ -98,19 +98,19 @@ /** | ||
export function parse(str, { prefixes = "./", delimiter = "/#?" } = {}) { | ||
var tokens = lexer(str) | ||
var tokens = lexer(str); | ||
var defaultPattern = "[^".concat(escapeString(delimiter), "]+?") | ||
var result = [] | ||
var key = 0 | ||
var i = 0 | ||
var path = "" | ||
var defaultPattern = "[^".concat(escapeString(delimiter), "]+?"); | ||
var result = []; | ||
var key = 0; | ||
var i = 0; | ||
var path = ""; | ||
var tryConsume = function (type) { | ||
if (i < tokens.length && tokens[i].type === type) return tokens[i++].value | ||
return undefined | ||
} | ||
if (i < tokens.length && tokens[i].type === type) return tokens[i++].value; | ||
return undefined; | ||
}; | ||
var mustConsume = function (type) { | ||
var value = tryConsume(type) | ||
if (value !== undefined) return value | ||
var _a = tokens[i] | ||
var nextType = _a.type | ||
var index = _a.inde | ||
var value = tryConsume(type); | ||
if (value !== undefined) return value; | ||
var _a = tokens[i]; | ||
var nextType = _a.type; | ||
var index = _a.inde; | ||
throw new TypeError( | ||
@@ -121,25 +121,25 @@ "Unexpected " | ||
.concat(type), | ||
) | ||
} | ||
); | ||
}; | ||
var consumeText = function () { | ||
var result = "" | ||
var value | ||
var result = ""; | ||
var value; | ||
while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { | ||
result += value | ||
result += value; | ||
} | ||
return result | ||
} | ||
return result; | ||
}; | ||
while (i < tokens.length) { | ||
var char = tryConsume("CHAR") | ||
var name = tryConsume("NAME") | ||
var pattern = tryConsume("PATTERN") | ||
var char = tryConsume("CHAR"); | ||
var name = tryConsume("NAME"); | ||
var pattern = tryConsume("PATTERN"); | ||
if (name || pattern) { | ||
let prefix = char || "" | ||
let prefix = char || ""; | ||
if (prefixes.indexOf(prefix) === -1) { | ||
path += prefix | ||
prefix = "" | ||
path += prefix; | ||
prefix = ""; | ||
} | ||
if (path) { | ||
result.push(path) | ||
path = "" | ||
result.push(path); | ||
path = ""; | ||
} | ||
@@ -152,21 +152,21 @@ result.push({ | ||
modifier: tryConsume("MODIFIER") || "", | ||
}) | ||
continue | ||
}); | ||
continue; | ||
} | ||
var value = char || tryConsume("ESCAPED_CHAR") | ||
var value = char || tryConsume("ESCAPED_CHAR"); | ||
if (value) { | ||
path += value | ||
continue | ||
path += value; | ||
continue; | ||
} | ||
if (path) { | ||
result.push(path) | ||
path = "" | ||
result.push(path); | ||
path = ""; | ||
} | ||
var open = tryConsume("OPEN") | ||
var open = tryConsume("OPEN"); | ||
if (open) { | ||
var prefix = consumeText() | ||
var name_1 = tryConsume("NAME") || "" | ||
var pattern_1 = tryConsume("PATTERN") || "" | ||
var suffix = consumeText() | ||
mustConsume("CLOSE") | ||
var prefix = consumeText(); | ||
var name_1 = tryConsume("NAME") || ""; | ||
var pattern_1 = tryConsume("PATTERN") || ""; | ||
var suffix = consumeText(); | ||
mustConsume("CLOSE"); | ||
result.push({ | ||
@@ -178,8 +178,8 @@ name: name_1 || (pattern_1 ? key++ : ""), | ||
modifier: tryConsume("MODIFIER") || "", | ||
}) | ||
continue | ||
}); | ||
continue; | ||
} | ||
mustConsume("END") | ||
mustConsume("END"); | ||
} | ||
return result | ||
return result; | ||
} | ||
@@ -190,3 +190,3 @@ /** | ||
export function compile(str, options) { | ||
return tokensToFunction(parse(str, options), options) | ||
return tokensToFunction(parse(str, options), options); | ||
} | ||
@@ -206,17 +206,17 @@ /** | ||
sensitive ? "" : "i", | ||
) | ||
); | ||
} | ||
return undefined | ||
}) | ||
return undefined; | ||
}); | ||
return function (data) { | ||
var path = "" | ||
var path = ""; | ||
for (var i = 0; i < tokens.length; i++) { | ||
var token = tokens[i] | ||
var token = tokens[i]; | ||
if (typeof token === "string") { | ||
path += token | ||
continue | ||
path += token; | ||
continue; | ||
} | ||
var value = data ? data[token.name] : undefined | ||
var optional = token.modifier === "?" || token.modifier === "*" | ||
var repeat = token.modifier === "*" || token.modifier === "+" | ||
var value = data ? data[token.name] : undefined; | ||
var optional = token.modifier === "?" || token.modifier === "*"; | ||
var repeat = token.modifier === "*" || token.modifier === "+"; | ||
if (Array.isArray(value)) { | ||
@@ -229,12 +229,12 @@ if (!repeat) { | ||
), | ||
) | ||
); | ||
} | ||
if (value.length === 0) { | ||
if (optional) continue | ||
if (optional) continue; | ||
throw new TypeError( | ||
'Expected "'.concat(token.name, '" to not be empty'), | ||
) | ||
); | ||
} | ||
for (var j = 0; j < value.length; j++) { | ||
let segment = encode(value[j], token) | ||
let segment = encode(value[j], token); | ||
if (validate && !matches[i].test(segment)) { | ||
@@ -246,10 +246,10 @@ throw new TypeError( | ||
.concat(segment, '"'), | ||
) | ||
); | ||
} | ||
path += token.prefix + segment + token.suffix | ||
path += token.prefix + segment + token.suffix; | ||
} | ||
continue | ||
continue; | ||
} | ||
if (typeof value === "string" || typeof value === "number") { | ||
var segment = encode(String(value), token) | ||
var segment = encode(String(value), token); | ||
if (validate && !matches[i].test(segment)) { | ||
@@ -261,15 +261,15 @@ throw new TypeError( | ||
.concat(segment, '"'), | ||
) | ||
); | ||
} | ||
path += token.prefix + segment + token.suffix | ||
continue | ||
path += token.prefix + segment + token.suffix; | ||
continue; | ||
} | ||
if (optional) continue | ||
var typeOfMessage = repeat ? "an array" : "a string" | ||
if (optional) continue; | ||
var typeOfMessage = repeat ? "an array" : "a string"; | ||
throw new TypeError( | ||
'Expected "'.concat(token.name, '" to be ').concat(typeOfMessage), | ||
) | ||
); | ||
} | ||
return path | ||
} | ||
return path; | ||
}; | ||
} | ||
@@ -280,5 +280,5 @@ /** | ||
export function match(str, options) { | ||
var keys = [] | ||
var re = pathToRegexp(str, keys, options) | ||
return regexpToFunction(re, keys, options) | ||
var keys = []; | ||
var re = pathToRegexp(str, keys, options); | ||
return regexpToFunction(re, keys, options); | ||
} | ||
@@ -290,10 +290,10 @@ /** | ||
return function (pathname) { | ||
var m = re.exec(pathname) | ||
if (!m) return false | ||
var path = m[0] | ||
var index = m.index | ||
var params = Object.create(null) | ||
var m = re.exec(pathname); | ||
if (!m) return false; | ||
var path = m[0]; | ||
var index = m.index; | ||
var params = Object.create(null); | ||
var _loop_1 = function (i) { | ||
if (m[i] === undefined) return "continue" | ||
var key = keys[i - 1] | ||
if (m[i] === undefined) return "continue"; | ||
var key = keys[i - 1]; | ||
if (key.modifier === "*" || key.modifier === "+") { | ||
@@ -303,14 +303,14 @@ params[key.name] = m[i] | ||
.map(function (value) { | ||
return decode(value, key) | ||
}) | ||
return decode(value, key); | ||
}); | ||
} else { | ||
params[key.name] = decode(m[i], key) | ||
params[key.name] = decode(m[i], key); | ||
} | ||
return undefined | ||
} | ||
return undefined; | ||
}; | ||
for (var i = 1; i < m.length; i++) { | ||
_loop_1(i) | ||
_loop_1(i); | ||
} | ||
return { path, index, params } | ||
} | ||
return { path, index, params }; | ||
}; | ||
} | ||
@@ -321,3 +321,3 @@ /** | ||
function escapeString(str) { | ||
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1") | ||
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); | ||
} | ||
@@ -328,6 +328,6 @@ /** | ||
function regexpToRegexp(path, keys) { | ||
if (!keys) return path | ||
var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g | ||
var index = 0 | ||
var execResult = groupsRegex.exec(path.source) | ||
if (!keys) return path; | ||
var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; | ||
var index = 0; | ||
var execResult = groupsRegex.exec(path.source); | ||
while (execResult) { | ||
@@ -341,6 +341,6 @@ keys.push({ | ||
pattern: "", | ||
}) | ||
execResult = groupsRegex.exec(path.source) | ||
}); | ||
execResult = groupsRegex.exec(path.source); | ||
} | ||
return path | ||
return path; | ||
} | ||
@@ -352,5 +352,5 @@ /** | ||
var parts = paths.map(function (path) { | ||
return pathToRegexp(path, keys, { sensitive }).source | ||
}) | ||
return new RegExp("(?:".concat(parts.join("|"), ")"), sensitive ? "" : "i") | ||
return pathToRegexp(path, keys, { sensitive }).source; | ||
}); | ||
return new RegExp("(?:".concat(parts.join("|"), ")"), sensitive ? "" : "i"); | ||
} | ||
@@ -361,3 +361,3 @@ /** | ||
function stringToRegexp(path, keys, options) { | ||
return tokensToRegexp(parse(path, options), keys, options) | ||
return tokensToRegexp(parse(path, options), keys, options); | ||
} | ||
@@ -380,18 +380,18 @@ /** | ||
) { | ||
var endsWithRe = "[".concat(escapeString(endsWith), "]|$") | ||
var delimiterRe = "[".concat(escapeString(delimiter), "]") | ||
var route = start ? "^" : "" | ||
var endsWithRe = "[".concat(escapeString(endsWith), "]|$"); | ||
var delimiterRe = "[".concat(escapeString(delimiter), "]"); | ||
var route = start ? "^" : ""; | ||
// Iterate over the tokens and create our regexp string. | ||
for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { | ||
var token = tokens_1[_i] | ||
var token = tokens_1[_i]; | ||
if (typeof token === "string") { | ||
route += escapeString(encode(token)) | ||
route += escapeString(encode(token)); | ||
} else { | ||
var prefix = escapeString(encode(token.prefix)) | ||
var suffix = escapeString(encode(token.suffix)) | ||
var prefix = escapeString(encode(token.prefix)); | ||
var suffix = escapeString(encode(token.suffix)); | ||
if (token.pattern) { | ||
if (keys) keys.push(token) | ||
if (keys) keys.push(token); | ||
if (prefix || suffix) { | ||
if (token.modifier === "+" || token.modifier === "*") { | ||
var mod = token.modifier === "*" ? "?" : "" | ||
var mod = token.modifier === "*" ? "?" : ""; | ||
route += "(?:" | ||
@@ -404,3 +404,3 @@ .concat(prefix, "((?:") | ||
.concat(suffix, ")") | ||
.concat(mod) | ||
.concat(mod); | ||
} else { | ||
@@ -411,11 +411,16 @@ route += "(?:" | ||
.concat(suffix, ")") | ||
.concat(token.modifier) | ||
.concat(token.modifier); | ||
} | ||
} else if (token.modifier === "+" || token.modifier === "*") { | ||
route += "((?:".concat(token.pattern, ")").concat(token.modifier, ")") | ||
route += "((?:" | ||
.concat(token.pattern, ")") | ||
.concat(token.modifier, ")"); | ||
} else { | ||
route += "(".concat(token.pattern, ")").concat(token.modifier) | ||
route += "(".concat(token.pattern, ")").concat(token.modifier); | ||
} | ||
} else { | ||
route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier) | ||
route += "(?:" | ||
.concat(prefix) | ||
.concat(suffix, ")") | ||
.concat(token.modifier); | ||
} | ||
@@ -425,18 +430,18 @@ } | ||
if (end) { | ||
if (!strict) route += "".concat(delimiterRe, "?") | ||
route += endsWith ? "(?=".concat(endsWithRe, ")") : "$" | ||
if (!strict) route += "".concat(delimiterRe, "?"); | ||
route += endsWith ? "(?=".concat(endsWithRe, ")") : "$"; | ||
} else { | ||
var endToken = tokens[tokens.length - 1] | ||
var endToken = tokens[tokens.length - 1]; | ||
var isEndDelimited = | ||
typeof endToken === "string" | ||
? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 | ||
: endToken === undefined | ||
: endToken === undefined; | ||
if (!strict) { | ||
route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?") | ||
route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?"); | ||
} | ||
if (!isEndDelimited) { | ||
route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")") | ||
route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")"); | ||
} | ||
} | ||
return new RegExp(route, sensitive ? "" : "i") | ||
return new RegExp(route, sensitive ? "" : "i"); | ||
} | ||
@@ -451,5 +456,5 @@ /** | ||
export function pathToRegexp(path, keys, options) { | ||
if (path instanceof RegExp) return regexpToRegexp(path, keys) | ||
if (Array.isArray(path)) return arrayToRegexp(path, keys, options) | ||
return stringToRegexp(path, keys, options) | ||
if (path instanceof RegExp) return regexpToRegexp(path, keys); | ||
if (Array.isArray(path)) return arrayToRegexp(path, keys, options); | ||
return stringToRegexp(path, keys, options); | ||
} |
@@ -1,2 +0,2 @@ | ||
import { match } from "./path_to_regexp.js" | ||
import { match } from "./path_to_regexp.js"; | ||
@@ -7,15 +7,15 @@ export const setupRoutes = (routes) => { | ||
decode: decodeURIComponent, | ||
}) | ||
}); | ||
return { | ||
applyPatternMatching, | ||
requestHandler: routes[pathPattern], | ||
} | ||
}) | ||
}; | ||
}); | ||
return (request, { pushResponse, redirectRequest }) => { | ||
let result | ||
let result; | ||
const found = candidates.find((candidate) => { | ||
result = candidate.applyPatternMatching(request.pathname) | ||
return Boolean(result) | ||
}) | ||
result = candidate.applyPatternMatching(request.pathname); | ||
return Boolean(result); | ||
}); | ||
if (found) { | ||
@@ -28,6 +28,6 @@ return found.requestHandler( | ||
{ pushResponse, redirectRequest }, | ||
) | ||
); | ||
} | ||
return null | ||
} | ||
} | ||
return null; | ||
}; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { timeStart } from "./server_timing/timing_measure.js" | ||
import { timeStart } from "./server_timing/timing_measure.js"; | ||
@@ -13,21 +13,21 @@ const HOOK_NAMES = [ | ||
"serverStopped", | ||
] | ||
]; | ||
export const createServiceController = (services) => { | ||
const flatServices = flattenAndFilterServices(services) | ||
const hookGroups = {} | ||
const flatServices = flattenAndFilterServices(services); | ||
const hookGroups = {}; | ||
const addService = (service) => { | ||
Object.keys(service).forEach((key) => { | ||
if (key === "name") return | ||
const isHook = HOOK_NAMES.includes(key) | ||
if (key === "name") return; | ||
const isHook = HOOK_NAMES.includes(key); | ||
if (!isHook) { | ||
console.warn( | ||
`Unexpected "${key}" property on "${service.name}" service`, | ||
) | ||
); | ||
} | ||
const hookName = key | ||
const hookValue = service[hookName] | ||
const hookName = key; | ||
const hookValue = service[hookName]; | ||
if (hookValue) { | ||
const group = hookGroups[hookName] || (hookGroups[hookName] = []) | ||
const group = hookGroups[hookName] || (hookGroups[hookName] = []); | ||
group.push({ | ||
@@ -37,66 +37,66 @@ service, | ||
value: hookValue, | ||
}) | ||
}); | ||
} | ||
}) | ||
} | ||
}); | ||
}; | ||
flatServices.forEach((service) => { | ||
addService(service) | ||
}) | ||
addService(service); | ||
}); | ||
let currentService = null | ||
let currentHookName = null | ||
let currentService = null; | ||
let currentHookName = null; | ||
const callHook = (hook, info, context) => { | ||
const hookFn = hook.value | ||
const hookFn = hook.value; | ||
if (!hookFn) { | ||
return null | ||
return null; | ||
} | ||
currentService = hook.service | ||
currentHookName = hook.name | ||
let timeEnd | ||
currentService = hook.service; | ||
currentHookName = hook.name; | ||
let timeEnd; | ||
if (context && context.timing) { | ||
timeEnd = timeStart( | ||
`${currentService.name.replace("jsenv:", "")}.${currentHookName}`, | ||
) | ||
); | ||
} | ||
let valueReturned = hookFn(info, context) | ||
let valueReturned = hookFn(info, context); | ||
if (context && context.timing) { | ||
Object.assign(context.timing, timeEnd()) | ||
Object.assign(context.timing, timeEnd()); | ||
} | ||
currentService = null | ||
currentHookName = null | ||
return valueReturned | ||
} | ||
currentService = null; | ||
currentHookName = null; | ||
return valueReturned; | ||
}; | ||
const callAsyncHook = async (hook, info, context) => { | ||
const hookFn = hook.value | ||
const hookFn = hook.value; | ||
if (!hookFn) { | ||
return null | ||
return null; | ||
} | ||
currentService = hook.service | ||
currentHookName = hook.name | ||
let timeEnd | ||
currentService = hook.service; | ||
currentHookName = hook.name; | ||
let timeEnd; | ||
if (context && context.timing) { | ||
timeEnd = timeStart( | ||
`${currentService.name.replace("jsenv:", "")}.${currentHookName}`, | ||
) | ||
); | ||
} | ||
let valueReturned = await hookFn(info, context) | ||
let valueReturned = await hookFn(info, context); | ||
if (context && context.timing) { | ||
Object.assign(context.timing, timeEnd()) | ||
Object.assign(context.timing, timeEnd()); | ||
} | ||
currentService = null | ||
currentHookName = null | ||
return valueReturned | ||
} | ||
currentService = null; | ||
currentHookName = null; | ||
return valueReturned; | ||
}; | ||
const callHooks = (hookName, info, context, callback = () => {}) => { | ||
const hooks = hookGroups[hookName] | ||
const hooks = hookGroups[hookName]; | ||
if (hooks) { | ||
for (const hook of hooks) { | ||
const returnValue = callHook(hook, info, context) | ||
const returnValue = callHook(hook, info, context); | ||
if (returnValue) { | ||
callback(returnValue) | ||
callback(returnValue); | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
const callHooksUntil = ( | ||
@@ -108,21 +108,21 @@ hookName, | ||
) => { | ||
const hooks = hookGroups[hookName] | ||
const hooks = hookGroups[hookName]; | ||
if (hooks) { | ||
for (const hook of hooks) { | ||
const returnValue = callHook(hook, info, context) | ||
const untilReturnValue = until(returnValue) | ||
const returnValue = callHook(hook, info, context); | ||
const untilReturnValue = until(returnValue); | ||
if (untilReturnValue) { | ||
return untilReturnValue | ||
return untilReturnValue; | ||
} | ||
} | ||
} | ||
return null | ||
} | ||
return null; | ||
}; | ||
const callAsyncHooksUntil = (hookName, info, context) => { | ||
const hooks = hookGroups[hookName] | ||
const hooks = hookGroups[hookName]; | ||
if (!hooks) { | ||
return null | ||
return null; | ||
} | ||
if (hooks.length === 0) { | ||
return null | ||
return null; | ||
} | ||
@@ -132,16 +132,16 @@ return new Promise((resolve, reject) => { | ||
if (index >= hooks.length) { | ||
return resolve() | ||
return resolve(); | ||
} | ||
const hook = hooks[index] | ||
const returnValue = callAsyncHook(hook, info, context) | ||
const hook = hooks[index]; | ||
const returnValue = callAsyncHook(hook, info, context); | ||
return Promise.resolve(returnValue).then((output) => { | ||
if (output) { | ||
return resolve(output) | ||
return resolve(output); | ||
} | ||
return visit(index + 1) | ||
}, reject) | ||
} | ||
visit(0) | ||
}) | ||
} | ||
return visit(index + 1); | ||
}, reject); | ||
}; | ||
visit(0); | ||
}); | ||
}; | ||
@@ -157,23 +157,23 @@ return { | ||
getCurrentHookName: () => currentHookName, | ||
} | ||
} | ||
}; | ||
}; | ||
const flattenAndFilterServices = (services) => { | ||
const flatServices = [] | ||
const flatServices = []; | ||
const visitServiceEntry = (serviceEntry) => { | ||
if (Array.isArray(serviceEntry)) { | ||
serviceEntry.forEach((value) => visitServiceEntry(value)) | ||
return | ||
serviceEntry.forEach((value) => visitServiceEntry(value)); | ||
return; | ||
} | ||
if (typeof serviceEntry === "object" && serviceEntry !== null) { | ||
if (!serviceEntry.name) { | ||
serviceEntry.name = "anonymous" | ||
serviceEntry.name = "anonymous"; | ||
} | ||
flatServices.push(serviceEntry) | ||
return | ||
flatServices.push(serviceEntry); | ||
return; | ||
} | ||
throw new Error(`services must be objects, got ${serviceEntry}`) | ||
} | ||
services.forEach((serviceEntry) => visitServiceEntry(serviceEntry)) | ||
return flatServices | ||
} | ||
throw new Error(`services must be objects, got ${serviceEntry}`); | ||
}; | ||
services.forEach((serviceEntry) => visitServiceEntry(serviceEntry)); | ||
return flatServices; | ||
}; |
@@ -1,2 +0,2 @@ | ||
export const jsenvAccessControlAllowedHeaders = ["x-requested-with"] | ||
export const jsenvAccessControlAllowedHeaders = ["x-requested-with"]; | ||
@@ -9,3 +9,3 @@ export const jsenvAccessControlAllowedMethods = [ | ||
"OPTIONS", | ||
] | ||
]; | ||
@@ -28,6 +28,6 @@ export const jsenvServiceCORS = ({ | ||
const corsEnabled = | ||
accessControlAllowRequestOrigin || accessControlAllowedOrigins.length | ||
accessControlAllowRequestOrigin || accessControlAllowedOrigins.length; | ||
if (!corsEnabled) { | ||
return [] | ||
return []; | ||
} | ||
@@ -47,5 +47,5 @@ | ||
}, | ||
} | ||
}; | ||
} | ||
return null | ||
return null; | ||
}, | ||
@@ -65,7 +65,7 @@ | ||
timingAllowOrigin, | ||
}) | ||
return accessControlHeaders | ||
}); | ||
return accessControlHeaders; | ||
}, | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -88,18 +88,18 @@ // https://www.w3.org/TR/cors/ | ||
} = {}) => { | ||
const vary = [] | ||
const vary = []; | ||
const allowedOriginArray = [...accessControlAllowedOrigins] | ||
const allowedOriginArray = [...accessControlAllowedOrigins]; | ||
if (accessControlAllowRequestOrigin) { | ||
if ("origin" in headers && headers.origin !== "null") { | ||
allowedOriginArray.push(headers.origin) | ||
vary.push("origin") | ||
allowedOriginArray.push(headers.origin); | ||
vary.push("origin"); | ||
} else if ("referer" in headers) { | ||
allowedOriginArray.push(new URL(headers.referer).origin) | ||
vary.push("referer") | ||
allowedOriginArray.push(new URL(headers.referer).origin); | ||
vary.push("referer"); | ||
} else { | ||
allowedOriginArray.push("*") | ||
allowedOriginArray.push("*"); | ||
} | ||
} | ||
const allowedMethodArray = [...accessControlAllowedMethods] | ||
const allowedMethodArray = [...accessControlAllowedMethods]; | ||
if ( | ||
@@ -109,10 +109,10 @@ accessControlAllowRequestMethod && | ||
) { | ||
const requestMethodName = headers["access-control-request-method"] | ||
const requestMethodName = headers["access-control-request-method"]; | ||
if (!allowedMethodArray.includes(requestMethodName)) { | ||
allowedMethodArray.push(requestMethodName) | ||
vary.push("access-control-request-method") | ||
allowedMethodArray.push(requestMethodName); | ||
vary.push("access-control-request-method"); | ||
} | ||
} | ||
const allowedHeaderArray = [...accessControlAllowedHeaders] | ||
const allowedHeaderArray = [...accessControlAllowedHeaders]; | ||
if ( | ||
@@ -123,12 +123,12 @@ accessControlAllowRequestHeaders && | ||
const requestHeaderNameArray = | ||
headers["access-control-request-headers"].split(", ") | ||
headers["access-control-request-headers"].split(", "); | ||
requestHeaderNameArray.forEach((headerName) => { | ||
const headerNameLowerCase = headerName.toLowerCase() | ||
const headerNameLowerCase = headerName.toLowerCase(); | ||
if (!allowedHeaderArray.includes(headerNameLowerCase)) { | ||
allowedHeaderArray.push(headerNameLowerCase) | ||
allowedHeaderArray.push(headerNameLowerCase); | ||
if (!vary.includes("access-control-request-headers")) { | ||
vary.push("access-control-request-headers") | ||
vary.push("access-control-request-headers"); | ||
} | ||
} | ||
}) | ||
}); | ||
} | ||
@@ -148,3 +148,3 @@ | ||
...(vary.length ? { vary: vary.join(", ") } : {}), | ||
} | ||
} | ||
}; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { pickContentType } from "../../content_negotiation/pick_content_type.js" | ||
import { pickContentType } from "../../content_negotiation/pick_content_type.js"; | ||
@@ -10,5 +10,5 @@ export const jsenvServiceErrorHandler = ({ sendErrorDetails = false } = {}) => { | ||
(typeof serverInternalError !== "object" && | ||
typeof serverInternalError !== "function") | ||
typeof serverInternalError !== "function"); | ||
if (!serverInternalErrorIsAPrimitive && serverInternalError.asResponse) { | ||
return serverInternalError.asResponse() | ||
return serverInternalError.asResponse(); | ||
} | ||
@@ -28,3 +28,3 @@ const dataToSend = serverInternalErrorIsAPrimitive | ||
: {}), | ||
} | ||
}; | ||
@@ -34,4 +34,4 @@ const availableContentTypes = { | ||
const renderHtmlForErrorWithoutDetails = () => { | ||
return `<p>Details not available: to enable them use jsenvServiceErrorHandler({ sendErrorDetails: true }).</p>` | ||
} | ||
return `<p>Details not available: to enable them use jsenvServiceErrorHandler({ sendErrorDetails: true }).</p>`; | ||
}; | ||
@@ -44,6 +44,6 @@ const renderHtmlForErrorWithDetails = () => { | ||
" ", | ||
)}</pre>` | ||
)}</pre>`; | ||
} | ||
return `<pre>${serverInternalError.stack}</pre>` | ||
} | ||
return `<pre>${serverInternalError.stack}</pre>`; | ||
}; | ||
@@ -74,3 +74,3 @@ const body = `<!DOCTYPE html> | ||
</body> | ||
</html>` | ||
</html>`; | ||
@@ -83,6 +83,6 @@ return { | ||
body, | ||
} | ||
}; | ||
}, | ||
"application/json": () => { | ||
const body = JSON.stringify(dataToSend) | ||
const body = JSON.stringify(dataToSend); | ||
return { | ||
@@ -94,12 +94,12 @@ headers: { | ||
body, | ||
} | ||
}; | ||
}, | ||
} | ||
}; | ||
const bestContentType = pickContentType( | ||
request, | ||
Object.keys(availableContentTypes), | ||
) | ||
return availableContentTypes[bestContentType || "application/json"]() | ||
); | ||
return availableContentTypes[bestContentType || "application/json"](); | ||
}, | ||
} | ||
} | ||
}; | ||
}; |
@@ -1,37 +0,37 @@ | ||
import { URL_META } from "@jsenv/url-meta" | ||
import { URL_META } from "@jsenv/url-meta"; | ||
export const jsenvServiceRequestAliases = (resourceAliases) => { | ||
const aliases = {} | ||
const aliases = {}; | ||
Object.keys(resourceAliases).forEach((key) => { | ||
aliases[asFileUrl(key)] = asFileUrl(resourceAliases[key]) | ||
}) | ||
aliases[asFileUrl(key)] = asFileUrl(resourceAliases[key]); | ||
}); | ||
return { | ||
name: "jsenv:request_aliases", | ||
redirectRequest: (request) => { | ||
const resourceBeforeAlias = request.resource | ||
const resourceBeforeAlias = request.resource; | ||
const urlAfterAliasing = URL_META.applyAliases({ | ||
url: asFileUrl(request.pathname), | ||
aliases, | ||
}) | ||
const resourceAfterAlias = urlAfterAliasing.slice("file://".length) | ||
}); | ||
const resourceAfterAlias = urlAfterAliasing.slice("file://".length); | ||
if (resourceBeforeAlias === resourceAfterAlias) { | ||
return null | ||
return null; | ||
} | ||
const resource = replaceResource(resourceBeforeAlias, resourceAfterAlias) | ||
return { resource } | ||
const resource = replaceResource(resourceBeforeAlias, resourceAfterAlias); | ||
return { resource }; | ||
}, | ||
} | ||
} | ||
}; | ||
}; | ||
const asFileUrl = (specifier) => new URL(specifier, "file:///").href | ||
const asFileUrl = (specifier) => new URL(specifier, "file:///").href; | ||
const replaceResource = (resourceBeforeAlias, newValue) => { | ||
const urlObject = new URL(resourceBeforeAlias, "file://") | ||
const searchSeparatorIndex = newValue.indexOf("?") | ||
const urlObject = new URL(resourceBeforeAlias, "file://"); | ||
const searchSeparatorIndex = newValue.indexOf("?"); | ||
if (searchSeparatorIndex > -1) { | ||
return newValue // let new value override search params | ||
return newValue; // let new value override search params | ||
} | ||
urlObject.pathname = newValue | ||
const resource = `${urlObject.pathname}${urlObject.search}` | ||
return resource | ||
} | ||
urlObject.pathname = newValue; | ||
const resource = `${urlObject.pathname}${urlObject.search}`; | ||
return resource; | ||
}; |
@@ -1,4 +0,4 @@ | ||
import { pickContentType } from "@jsenv/server/src/content_negotiation/pick_content_type.js" | ||
import { pickContentLanguage } from "@jsenv/server/src/content_negotiation/pick_content_language.js" | ||
import { pickContentEncoding } from "@jsenv/server/src/content_negotiation/pick_content_encoding.js" | ||
import { pickContentType } from "@jsenv/server/src/content_negotiation/pick_content_type.js"; | ||
import { pickContentLanguage } from "@jsenv/server/src/content_negotiation/pick_content_language.js"; | ||
import { pickContentEncoding } from "@jsenv/server/src/content_negotiation/pick_content_encoding.js"; | ||
@@ -9,10 +9,10 @@ export const jsenvServiceResponseAcceptanceCheck = () => { | ||
inspectResponse: (request, { response, warn }) => { | ||
checkResponseAcceptance(request, response, { warn }) | ||
checkResponseAcceptance(request, response, { warn }); | ||
}, | ||
} | ||
} | ||
}; | ||
}; | ||
const checkResponseAcceptance = (request, response, { warn }) => { | ||
const requestAcceptHeader = request.headers.accept | ||
const responseContentTypeHeader = response.headers["content-type"] | ||
const requestAcceptHeader = request.headers.accept; | ||
const responseContentTypeHeader = response.headers["content-type"]; | ||
if ( | ||
@@ -27,7 +27,7 @@ requestAcceptHeader && | ||
--- request accept header --- | ||
${requestAcceptHeader}`) | ||
${requestAcceptHeader}`); | ||
} | ||
const requestAcceptLanguageHeader = request.headers["accept-language"] | ||
const responseContentLanguageHeader = response.headers["content-language"] | ||
const requestAcceptLanguageHeader = request.headers["accept-language"]; | ||
const responseContentLanguageHeader = response.headers["content-language"]; | ||
if ( | ||
@@ -42,7 +42,7 @@ requestAcceptLanguageHeader && | ||
--- request accept-language header --- | ||
${requestAcceptLanguageHeader}`) | ||
${requestAcceptLanguageHeader}`); | ||
} | ||
const requestAcceptEncodingHeader = request.headers["accept-encoding"] | ||
const responseContentEncodingHeader = response.headers["content-encoding"] | ||
const requestAcceptEncodingHeader = request.headers["accept-encoding"]; | ||
const responseContentEncodingHeader = response.headers["content-encoding"]; | ||
if ( | ||
@@ -57,4 +57,4 @@ requestAcceptLanguageHeader && | ||
--- request accept-encoding header --- | ||
${requestAcceptEncodingHeader}`) | ||
${requestAcceptEncodingHeader}`); | ||
} | ||
} | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { createLogger } from "@jsenv/log" | ||
import { createLogger } from "@jsenv/log"; | ||
@@ -6,3 +6,3 @@ import { | ||
createCompositeProducer, | ||
} from "@jsenv/server/src/interfacing_with_node/observable.js" | ||
} from "@jsenv/server/src/interfacing_with_node/observable.js"; | ||
@@ -23,14 +23,14 @@ // https://www.html5rocks.com/en/tutorials/eventsource/basics/ | ||
} = {}) => { | ||
const logger = createLogger({ logLevel }) | ||
const logger = createLogger({ logLevel }); | ||
const room = {} | ||
const clients = new Set() | ||
const eventHistory = createEventHistory(historyLength) | ||
const room = {}; | ||
const clients = new Set(); | ||
const eventHistory = createEventHistory(historyLength); | ||
// what about previousEventId that keeps growing ? | ||
// we could add some limit | ||
// one limit could be that an event older than 24h is deleted | ||
let previousEventId = 0 | ||
let opened = false | ||
let interval | ||
let cleanupEffect = CLEANUP_NOOP | ||
let previousEventId = 0; | ||
let opened = false; | ||
let interval; | ||
let cleanupEffect = CLEANUP_NOOP; | ||
@@ -42,3 +42,3 @@ const join = (request) => { | ||
request.headers["last-event-id"] || | ||
new URL(request.url).searchParams.get("last-event-id") | ||
new URL(request.url).searchParams.get("last-event-id"); | ||
@@ -48,3 +48,3 @@ if (clients.size >= maxClientAllowed) { | ||
status: 503, | ||
} | ||
}; | ||
} | ||
@@ -55,3 +55,3 @@ | ||
status: 204, | ||
} | ||
}; | ||
} | ||
@@ -64,26 +64,26 @@ | ||
request, | ||
} | ||
}; | ||
if (clients.size === 0) { | ||
const effectReturnValue = effect() | ||
const effectReturnValue = effect(); | ||
if (typeof effectReturnValue === "function") { | ||
cleanupEffect = effectReturnValue | ||
cleanupEffect = effectReturnValue; | ||
} else { | ||
cleanupEffect = CLEANUP_NOOP | ||
cleanupEffect = CLEANUP_NOOP; | ||
} | ||
} | ||
clients.add(client) | ||
clients.add(client); | ||
logger.debug( | ||
`A client has joined. Number of client in room: ${clients.size}`, | ||
) | ||
); | ||
if (lastKnownId !== undefined) { | ||
const previousEvents = getAllEventSince(lastKnownId) | ||
const eventMissedCount = previousEvents.length | ||
const previousEvents = getAllEventSince(lastKnownId); | ||
const eventMissedCount = previousEvents.length; | ||
if (eventMissedCount > 0) { | ||
logger.info( | ||
`send ${eventMissedCount} event missed by client since event with id "${lastKnownId}"`, | ||
) | ||
); | ||
previousEvents.forEach((previousEvent) => { | ||
next(stringifySourceEvent(previousEvent)) | ||
}) | ||
next(stringifySourceEvent(previousEvent)); | ||
}); | ||
} | ||
@@ -97,4 +97,4 @@ } | ||
data: new Date().toLocaleTimeString(), | ||
} | ||
addEventToHistory(welcomeEvent) | ||
}; | ||
addEventToHistory(welcomeEvent); | ||
@@ -105,7 +105,7 @@ // send to everyone | ||
history: false, | ||
}) | ||
}); | ||
} | ||
// send only to this client | ||
else { | ||
next(stringifySourceEvent(welcomeEvent)) | ||
next(stringifySourceEvent(welcomeEvent)); | ||
} | ||
@@ -117,15 +117,17 @@ } else { | ||
data: new Date().toLocaleTimeString(), | ||
} | ||
next(stringifySourceEvent(firstEvent)) | ||
}; | ||
next(stringifySourceEvent(firstEvent)); | ||
} | ||
return () => { | ||
clients.delete(client) | ||
clients.delete(client); | ||
if (clients.size === 0) { | ||
cleanupEffect() | ||
cleanupEffect = CLEANUP_NOOP | ||
cleanupEffect(); | ||
cleanupEffect = CLEANUP_NOOP; | ||
} | ||
logger.debug(`A client left. Number of client in room: ${clients.size}`) | ||
} | ||
}) | ||
logger.debug( | ||
`A client left. Number of client in room: ${clients.size}`, | ||
); | ||
}; | ||
}); | ||
@@ -136,3 +138,3 @@ const requestSSEObservable = connectRequestAndRoom( | ||
sseRoomObservable, | ||
) | ||
); | ||
@@ -147,37 +149,37 @@ return { | ||
body: requestSSEObservable, | ||
} | ||
} | ||
}; | ||
}; | ||
const leave = (request) => { | ||
disconnectRequestFromRoom(request, room) | ||
} | ||
disconnectRequestFromRoom(request, room); | ||
}; | ||
const addEventToHistory = (event) => { | ||
if (typeof event.id === "undefined") { | ||
event.id = computeEventId(event, previousEventId) | ||
event.id = computeEventId(event, previousEventId); | ||
} | ||
previousEventId = event.id | ||
eventHistory.add(event) | ||
} | ||
previousEventId = event.id; | ||
eventHistory.add(event); | ||
}; | ||
const sendEventToAllClients = (event, { history = true } = {}) => { | ||
if (history) { | ||
addEventToHistory(event) | ||
addEventToHistory(event); | ||
} | ||
logger.debug( | ||
`send "${event.type}" event to ${clients.size} client in the room`, | ||
) | ||
const eventString = stringifySourceEvent(event) | ||
); | ||
const eventString = stringifySourceEvent(event); | ||
clients.forEach((client) => { | ||
client.next(eventString) | ||
}) | ||
} | ||
client.next(eventString); | ||
}); | ||
}; | ||
const getAllEventSince = (id) => { | ||
const events = eventHistory.since(id) | ||
const events = eventHistory.since(id); | ||
if (welcomeEventEnabled && !welcomeEventPublic) { | ||
return events.filter((event) => event.type !== "welcome") | ||
return events.filter((event) => event.type !== "welcome"); | ||
} | ||
return events | ||
} | ||
return events; | ||
}; | ||
@@ -188,3 +190,3 @@ const keepAlive = () => { | ||
`send keep alive event, number of client listening event source: ${clients.size}`, | ||
) | ||
); | ||
sendEventToAllClients( | ||
@@ -196,25 +198,25 @@ { | ||
{ history: false }, | ||
) | ||
} | ||
); | ||
}; | ||
const open = () => { | ||
if (opened) return | ||
opened = true | ||
interval = setInterval(keepAlive, keepaliveDuration) | ||
if (opened) return; | ||
opened = true; | ||
interval = setInterval(keepAlive, keepaliveDuration); | ||
if (!keepProcessAlive) { | ||
interval.unref() | ||
interval.unref(); | ||
} | ||
} | ||
}; | ||
const close = () => { | ||
if (!opened) return | ||
logger.debug(`closing room, number of client in the room: ${clients.size}`) | ||
clients.forEach((client) => client.complete()) | ||
clients.clear() | ||
clearInterval(interval) | ||
eventHistory.reset() | ||
opened = false | ||
} | ||
if (!opened) return; | ||
logger.debug(`closing room, number of client in the room: ${clients.size}`); | ||
clients.forEach((client) => client.complete()); | ||
clients.clear(); | ||
clearInterval(interval); | ||
eventHistory.reset(); | ||
opened = false; | ||
}; | ||
open() | ||
open(); | ||
@@ -237,43 +239,43 @@ Object.assign(room, { | ||
open, | ||
}) | ||
return room | ||
} | ||
}); | ||
return room; | ||
}; | ||
const CLEANUP_NOOP = () => {} | ||
const CLEANUP_NOOP = () => {}; | ||
const requestMap = new Map() | ||
const requestMap = new Map(); | ||
const connectRequestAndRoom = (request, room, roomObservable) => { | ||
let sseProducer | ||
let roomObservableMap | ||
const requestInfo = requestMap.get(request) | ||
let sseProducer; | ||
let roomObservableMap; | ||
const requestInfo = requestMap.get(request); | ||
if (requestInfo) { | ||
sseProducer = requestInfo.sseProducer | ||
roomObservableMap = requestInfo.roomObservableMap | ||
sseProducer = requestInfo.sseProducer; | ||
roomObservableMap = requestInfo.roomObservableMap; | ||
} else { | ||
sseProducer = createCompositeProducer({ | ||
cleanup: () => { | ||
requestMap.delete(request) | ||
requestMap.delete(request); | ||
}, | ||
}) | ||
roomObservableMap = new Map() | ||
requestMap.set(request, { sseProducer, roomObservableMap }) | ||
}); | ||
roomObservableMap = new Map(); | ||
requestMap.set(request, { sseProducer, roomObservableMap }); | ||
} | ||
roomObservableMap.set(room, roomObservable) | ||
sseProducer.addObservable(roomObservable) | ||
roomObservableMap.set(room, roomObservable); | ||
sseProducer.addObservable(roomObservable); | ||
return createObservable(sseProducer) | ||
} | ||
return createObservable(sseProducer); | ||
}; | ||
const disconnectRequestFromRoom = (request, room) => { | ||
const requestInfo = requestMap.get(request) | ||
const requestInfo = requestMap.get(request); | ||
if (!requestInfo) { | ||
return | ||
return; | ||
} | ||
const { sseProducer, roomObservableMap } = requestInfo | ||
const roomObservable = roomObservableMap.get(room) | ||
roomObservableMap.delete(room) | ||
sseProducer.removeObservable(roomObservable) | ||
} | ||
const { sseProducer, roomObservableMap } = requestInfo; | ||
const roomObservable = roomObservableMap.get(room); | ||
roomObservableMap.delete(room); | ||
sseProducer.removeObservable(roomObservable); | ||
}; | ||
@@ -285,42 +287,42 @@ // https://github.com/dmail-old/project/commit/da7d2c88fc8273850812972885d030a22f9d7448 | ||
const stringifySourceEvent = ({ data, type = "message", id, retry }) => { | ||
let string = "" | ||
let string = ""; | ||
if (id !== undefined) { | ||
string += `id:${id}\n` | ||
string += `id:${id}\n`; | ||
} | ||
if (retry) { | ||
string += `retry:${retry}\n` | ||
string += `retry:${retry}\n`; | ||
} | ||
if (type !== "message") { | ||
string += `event:${type}\n` | ||
string += `event:${type}\n`; | ||
} | ||
string += `data:${data}\n\n` | ||
string += `data:${data}\n\n`; | ||
return string | ||
} | ||
return string; | ||
}; | ||
const createEventHistory = (limit) => { | ||
const events = [] | ||
const events = []; | ||
const add = (data) => { | ||
events.push(data) | ||
events.push(data); | ||
if (events.length >= limit) { | ||
events.shift() | ||
events.shift(); | ||
} | ||
} | ||
}; | ||
const since = (id) => { | ||
const index = events.findIndex((event) => String(event.id) === id) | ||
return index === -1 ? [] : events.slice(index + 1) | ||
} | ||
const index = events.findIndex((event) => String(event.id) === id); | ||
return index === -1 ? [] : events.slice(index + 1); | ||
}; | ||
const reset = () => { | ||
events.length = 0 | ||
} | ||
events.length = 0; | ||
}; | ||
return { add, since, reset } | ||
} | ||
return { add, since, reset }; | ||
}; |
@@ -1,4 +0,4 @@ | ||
import { isIP } from "node:net" | ||
import cluster from "node:cluster" | ||
import { createDetailedMessage, createLogger } from "@jsenv/log" | ||
import { isIP } from "node:net"; | ||
import cluster from "node:cluster"; | ||
import { createDetailedMessage, createLogger } from "@jsenv/log"; | ||
import { | ||
@@ -8,10 +8,10 @@ Abort, | ||
createCallbackListNotifiedOnce, | ||
} from "@jsenv/abort" | ||
import { memoize } from "@jsenv/utils/src/memoize/memoize.js" | ||
} from "@jsenv/abort"; | ||
import { memoize } from "@jsenv/utils/src/memoize/memoize.js"; | ||
import { createServiceController } from "./service_controller.js" | ||
import { timingToServerTimingResponseHeaders } from "./server_timing/timing_header.js" | ||
import { createPolyglotServer } from "./internal/server-polyglot.js" | ||
import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js" | ||
import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js" | ||
import { createServiceController } from "./service_controller.js"; | ||
import { timingToServerTimingResponseHeaders } from "./server_timing/timing_header.js"; | ||
import { createPolyglotServer } from "./internal/server-polyglot.js"; | ||
import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js"; | ||
import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js"; | ||
import { | ||
@@ -21,13 +21,13 @@ fromNodeRequest, | ||
applyRedirectionToRequest, | ||
} from "./interfacing_with_node/request_factory.js" | ||
import { writeNodeResponse } from "./interfacing_with_node/write_node_response.js" | ||
} from "./interfacing_with_node/request_factory.js"; | ||
import { writeNodeResponse } from "./interfacing_with_node/write_node_response.js"; | ||
import { | ||
statusToType, | ||
colorizeResponseStatus, | ||
} from "./internal/colorizeResponseStatus.js" | ||
import { listen, stopListening } from "./internal/listen.js" | ||
import { composeTwoResponses } from "./internal/response_composition.js" | ||
import { listenRequest } from "./internal/listenRequest.js" | ||
import { listenEvent } from "./internal/listenEvent.js" | ||
import { listenServerConnectionError } from "./internal/listenServerConnectionError.js" | ||
} from "./internal/colorizeResponseStatus.js"; | ||
import { listen, stopListening } from "./internal/listen.js"; | ||
import { composeTwoResponses } from "./internal/response_composition.js"; | ||
import { listenRequest } from "./internal/listenRequest.js"; | ||
import { listenEvent } from "./internal/listenEvent.js"; | ||
import { listenServerConnectionError } from "./internal/listenServerConnectionError.js"; | ||
import { | ||
@@ -41,8 +41,8 @@ STOP_REASON_INTERNAL_ERROR, | ||
STOP_REASON_NOT_SPECIFIED, | ||
} from "./stopReasons.js" | ||
import { composeTwoHeaders } from "./internal/headers_composition.js" | ||
} from "./stopReasons.js"; | ||
import { composeTwoHeaders } from "./internal/headers_composition.js"; | ||
import { createIpGetters } from "./internal/server_ips.js" | ||
import { parseHostname } from "./internal/hostname_parser.js" | ||
import { applyDnsResolution } from "./internal/dns_resolution.js" | ||
import { createIpGetters } from "./internal/server_ips.js"; | ||
import { parseHostname } from "./internal/hostname_parser.js"; | ||
import { applyDnsResolution } from "./internal/dns_resolution.js"; | ||
@@ -88,3 +88,3 @@ export const startServer = async ({ | ||
), | ||
) | ||
); | ||
}, | ||
@@ -102,24 +102,24 @@ // timeAllocated to start responding to a request | ||
{ | ||
const unexpectedParamNames = Object.keys(rest) | ||
const unexpectedParamNames = Object.keys(rest); | ||
if (unexpectedParamNames.length > 0) { | ||
throw new TypeError( | ||
`${unexpectedParamNames.join(",")}: there is no such param`, | ||
) | ||
); | ||
} | ||
if (https) { | ||
if (typeof https !== "object") { | ||
throw new TypeError(`https must be an object, got ${https}`) | ||
throw new TypeError(`https must be an object, got ${https}`); | ||
} | ||
const { certificate, privateKey } = https | ||
const { certificate, privateKey } = https; | ||
if (!certificate || !privateKey) { | ||
throw new TypeError( | ||
`https must be an object with { certificate, privateKey }`, | ||
) | ||
); | ||
} | ||
} | ||
if (http2 && !https) { | ||
throw new Error(`http2 needs https`) | ||
throw new Error(`http2 needs https`); | ||
} | ||
} | ||
const logger = createLogger({ logLevel }) | ||
const logger = createLogger({ logLevel }); | ||
// param warnings and normalization | ||
@@ -132,7 +132,7 @@ { | ||
) { | ||
redirectHttpToHttps = true | ||
redirectHttpToHttps = true; | ||
} | ||
if (redirectHttpToHttps && !https) { | ||
logger.warn(`redirectHttpToHttps ignored because protocol is http`) | ||
redirectHttpToHttps = false | ||
logger.warn(`redirectHttpToHttps ignored because protocol is http`); | ||
redirectHttpToHttps = false; | ||
} | ||
@@ -142,14 +142,14 @@ if (allowHttpRequestOnHttps && redirectHttpToHttps) { | ||
`redirectHttpToHttps ignored because allowHttpRequestOnHttps is enabled`, | ||
) | ||
redirectHttpToHttps = false | ||
); | ||
redirectHttpToHttps = false; | ||
} | ||
if (allowHttpRequestOnHttps && !https) { | ||
logger.warn(`allowHttpRequestOnHttps ignored because protocol is http`) | ||
allowHttpRequestOnHttps = false | ||
logger.warn(`allowHttpRequestOnHttps ignored because protocol is http`); | ||
allowHttpRequestOnHttps = false; | ||
} | ||
} | ||
const server = {} | ||
const serviceController = createServiceController(services) | ||
const server = {}; | ||
const serviceController = createServiceController(services); | ||
const processTeardownEvents = { | ||
@@ -161,21 +161,21 @@ SIGHUP: stopOnExit, | ||
exit: stopOnExit, | ||
} | ||
}; | ||
let status = "starting" | ||
let nodeServer | ||
const startServerOperation = Abort.startOperation() | ||
const stopCallbackList = createCallbackListNotifiedOnce() | ||
let status = "starting"; | ||
let nodeServer; | ||
const startServerOperation = Abort.startOperation(); | ||
const stopCallbackList = createCallbackListNotifiedOnce(); | ||
const serverOrigins = { | ||
local: "", // favors hostname when possible | ||
} | ||
}; | ||
try { | ||
startServerOperation.addAbortSignal(signal) | ||
startServerOperation.addAbortSignal(signal); | ||
startServerOperation.addAbortSource((abort) => { | ||
return raceProcessTeardownEvents(processTeardownEvents, ({ name }) => { | ||
logger.info(`process teardown (${name}) -> aborting start server`) | ||
abort() | ||
}) | ||
}) | ||
startServerOperation.throwIfAborted() | ||
logger.info(`process teardown (${name}) -> aborting start server`); | ||
abort(); | ||
}); | ||
}); | ||
startServerOperation.throwIfAborted(); | ||
nodeServer = await createNodeServer({ | ||
@@ -187,31 +187,31 @@ https, | ||
http1Allowed, | ||
}) | ||
startServerOperation.throwIfAborted() | ||
}); | ||
startServerOperation.throwIfAborted(); | ||
// https://nodejs.org/api/net.html#net_server_unref | ||
if (!keepProcessAlive) { | ||
nodeServer.unref() | ||
nodeServer.unref(); | ||
} | ||
const createOrigin = (hostname) => { | ||
const protocol = https ? "https" : "http" | ||
const protocol = https ? "https" : "http"; | ||
if (isIP(hostname) === 6) { | ||
return `${protocol}://[${hostname}]` | ||
return `${protocol}://[${hostname}]`; | ||
} | ||
return `${protocol}://${hostname}` | ||
} | ||
return `${protocol}://${hostname}`; | ||
}; | ||
const ipGetters = createIpGetters() | ||
let hostnameToListen | ||
const ipGetters = createIpGetters(); | ||
let hostnameToListen; | ||
if (acceptAnyIp) { | ||
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 }) | ||
serverOrigins.local = createOrigin(firstInternalIp) | ||
serverOrigins.localip = createOrigin(firstInternalIp) | ||
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 }) | ||
serverOrigins.externalip = createOrigin(firstExternalIp) | ||
hostnameToListen = preferIpv6 ? "::" : "0.0.0.0" | ||
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 }); | ||
serverOrigins.local = createOrigin(firstInternalIp); | ||
serverOrigins.localip = createOrigin(firstInternalIp); | ||
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 }); | ||
serverOrigins.externalip = createOrigin(firstExternalIp); | ||
hostnameToListen = preferIpv6 ? "::" : "0.0.0.0"; | ||
} else { | ||
hostnameToListen = hostname | ||
hostnameToListen = hostname; | ||
} | ||
const hostnameInfo = parseHostname(hostname) | ||
const hostnameInfo = parseHostname(hostname); | ||
if (hostnameInfo.type === "ip") { | ||
@@ -221,16 +221,16 @@ if (acceptAnyIp) { | ||
`hostname cannot be an ip when acceptAnyIp is enabled, got ${hostname}`, | ||
) | ||
); | ||
} | ||
preferIpv6 = hostnameInfo.version === 6 | ||
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 }) | ||
serverOrigins.local = createOrigin(firstInternalIp) | ||
serverOrigins.localip = createOrigin(firstInternalIp) | ||
preferIpv6 = hostnameInfo.version === 6; | ||
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 }); | ||
serverOrigins.local = createOrigin(firstInternalIp); | ||
serverOrigins.localip = createOrigin(firstInternalIp); | ||
if (hostnameInfo.label === "unspecified") { | ||
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 }) | ||
serverOrigins.externalip = createOrigin(firstExternalIp) | ||
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 }); | ||
serverOrigins.externalip = createOrigin(firstExternalIp); | ||
} else if (hostnameInfo.label === "loopback") { | ||
// nothing | ||
} else { | ||
serverOrigins.local = createOrigin(hostname) | ||
serverOrigins.local = createOrigin(hostname); | ||
} | ||
@@ -240,14 +240,14 @@ } else { | ||
verbatim: true, | ||
}) | ||
}); | ||
if (hostnameDnsResolution) { | ||
const hostnameIp = hostnameDnsResolution.address | ||
serverOrigins.localip = createOrigin(hostnameIp) | ||
serverOrigins.local = createOrigin(hostname) | ||
const hostnameIp = hostnameDnsResolution.address; | ||
serverOrigins.localip = createOrigin(hostnameIp); | ||
serverOrigins.local = createOrigin(hostname); | ||
} else { | ||
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 }) | ||
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 }); | ||
// fallback to internal ip because there is no ip | ||
// associated to this hostname on operating system (in hosts file) | ||
hostname = firstInternalIp | ||
hostnameToListen = firstInternalIp | ||
serverOrigins.local = createOrigin(firstInternalIp) | ||
hostname = firstInternalIp; | ||
hostnameToListen = firstInternalIp; | ||
serverOrigins.local = createOrigin(firstInternalIp); | ||
} | ||
@@ -262,16 +262,16 @@ } | ||
hostname: hostnameToListen, | ||
}) | ||
}); | ||
// normalize origins (remove :80 when port is 80 for instance) | ||
Object.keys(serverOrigins).forEach((key) => { | ||
serverOrigins[key] = new URL(`${serverOrigins[key]}:${port}`).origin | ||
}) | ||
serverOrigins[key] = new URL(`${serverOrigins[key]}:${port}`).origin; | ||
}); | ||
serviceController.callHooks("serverListening", { port }) | ||
serviceController.callHooks("serverListening", { port }); | ||
startServerOperation.addAbortCallback(async () => { | ||
await stopListening(nodeServer) | ||
}) | ||
startServerOperation.throwIfAborted() | ||
await stopListening(nodeServer); | ||
}); | ||
startServerOperation.throwIfAborted(); | ||
} finally { | ||
await startServerOperation.end() | ||
await startServerOperation.end(); | ||
} | ||
@@ -289,3 +289,3 @@ | ||
// over the ip | ||
const serverOrigin = serverOrigins.local | ||
const serverOrigin = serverOrigins.local; | ||
@@ -297,18 +297,18 @@ // now the server is started (listening) it cannot be aborted anymore | ||
stopCallbackList.add(({ reason }) => { | ||
logger.info(`${serverName} stopping server (reason: ${reason})`) | ||
}) | ||
logger.info(`${serverName} stopping server (reason: ${reason})`); | ||
}); | ||
stopCallbackList.add(async () => { | ||
await stopListening(nodeServer) | ||
}) | ||
let stoppedResolve | ||
await stopListening(nodeServer); | ||
}); | ||
let stoppedResolve; | ||
const stoppedPromise = new Promise((resolve) => { | ||
stoppedResolve = resolve | ||
}) | ||
stoppedResolve = resolve; | ||
}); | ||
const stop = memoize(async (reason = STOP_REASON_NOT_SPECIFIED) => { | ||
status = "stopping" | ||
await Promise.all(stopCallbackList.notify({ reason })) | ||
serviceController.callHooks("serverStopped", { reason }) | ||
status = "stopped" | ||
stoppedResolve(reason) | ||
}) | ||
status = "stopping"; | ||
await Promise.all(stopCallbackList.notify({ reason })); | ||
serviceController.callHooks("serverStopped", { reason }); | ||
status = "stopped"; | ||
stoppedResolve(reason); | ||
}); | ||
@@ -318,15 +318,15 @@ const cancelProcessTeardownRace = raceProcessTeardownEvents( | ||
(winner) => { | ||
stop(PROCESS_TEARDOWN_EVENTS_MAP[winner.name]) | ||
stop(PROCESS_TEARDOWN_EVENTS_MAP[winner.name]); | ||
}, | ||
) | ||
stopCallbackList.add(cancelProcessTeardownRace) | ||
); | ||
stopCallbackList.add(cancelProcessTeardownRace); | ||
const onError = (error) => { | ||
if (status === "stopping" && error.code === "ECONNRESET") { | ||
return | ||
return; | ||
} | ||
throw error | ||
} | ||
throw error; | ||
}; | ||
status = "opened" | ||
status = "opened"; | ||
@@ -336,14 +336,14 @@ const removeConnectionErrorListener = listenServerConnectionError( | ||
onError, | ||
) | ||
stopCallbackList.add(removeConnectionErrorListener) | ||
); | ||
stopCallbackList.add(removeConnectionErrorListener); | ||
const connectionsTracker = trackServerPendingConnections(nodeServer, { | ||
http2, | ||
}) | ||
}); | ||
// opened connection must be shutdown before the close event is emitted | ||
stopCallbackList.add(connectionsTracker.stop) | ||
stopCallbackList.add(connectionsTracker.stop); | ||
const pendingRequestsTracker = trackServerPendingRequests(nodeServer, { | ||
http2, | ||
}) | ||
}); | ||
// ensure pending requests got a response from the server | ||
@@ -354,4 +354,4 @@ stopCallbackList.add((reason) => { | ||
reason, | ||
}) | ||
}) | ||
}); | ||
}); | ||
@@ -363,5 +363,5 @@ request: { | ||
// might be closed when we'll try to attach "data" and "end" listeners to it | ||
nodeRequest.pause() | ||
nodeRequest.pause(); | ||
if (!nagle) { | ||
nodeRequest.connection.setNoDelay(true) | ||
nodeRequest.connection.setNoDelay(true); | ||
} | ||
@@ -371,31 +371,31 @@ if (redirectHttpToHttps && !nodeRequest.connection.encrypted) { | ||
location: `${serverOrigin}${nodeRequest.url}`, | ||
}) | ||
nodeResponse.end() | ||
return | ||
}); | ||
nodeResponse.end(); | ||
return; | ||
} | ||
const receiveRequestOperation = Abort.startOperation() | ||
const receiveRequestOperation = Abort.startOperation(); | ||
receiveRequestOperation.addAbortSource((abort) => { | ||
const closeEventCallback = () => { | ||
if (nodeRequest.complete) { | ||
receiveRequestOperation.end() | ||
receiveRequestOperation.end(); | ||
} else { | ||
nodeResponse.destroy() | ||
abort() | ||
nodeResponse.destroy(); | ||
abort(); | ||
} | ||
} | ||
nodeRequest.once("close", closeEventCallback) | ||
}; | ||
nodeRequest.once("close", closeEventCallback); | ||
return () => { | ||
nodeRequest.removeListener("close", closeEventCallback) | ||
} | ||
}) | ||
nodeRequest.removeListener("close", closeEventCallback); | ||
}; | ||
}); | ||
receiveRequestOperation.addAbortSource((abort) => { | ||
return stopCallbackList.add(abort) | ||
}) | ||
return stopCallbackList.add(abort); | ||
}); | ||
const sendResponseOperation = Abort.startOperation() | ||
sendResponseOperation.addAbortSignal(receiveRequestOperation.signal) | ||
const sendResponseOperation = Abort.startOperation(); | ||
sendResponseOperation.addAbortSignal(receiveRequestOperation.signal); | ||
sendResponseOperation.addAbortSource((abort) => { | ||
return stopCallbackList.add(abort) | ||
}) | ||
return stopCallbackList.add(abort); | ||
}); | ||
@@ -405,3 +405,3 @@ const request = fromNodeRequest(nodeRequest, { | ||
signal: receiveRequestOperation.signal, | ||
}) | ||
}); | ||
@@ -415,14 +415,14 @@ // Handling request is asynchronous, we buffer logs for that request | ||
children: [], | ||
} | ||
}; | ||
const addRequestLog = (node, { type, value }) => { | ||
node.logs.push({ type, value }) | ||
} | ||
node.logs.push({ type, value }); | ||
}; | ||
const onRequestHandled = (node) => { | ||
if (node !== rootRequestNode) { | ||
// keep buffering until root request write logs for everyone | ||
return | ||
return; | ||
} | ||
const prefixLines = (string, prefix) => { | ||
return string.replace(/^(?!\s*$)/gm, prefix) | ||
} | ||
return string.replace(/^(?!\s*$)/gm, prefix); | ||
}; | ||
const writeLog = ( | ||
@@ -433,36 +433,36 @@ { type, value }, | ||
if (depth > 0) { | ||
value = prefixLines(value, " ".repeat(depth)) | ||
value = prefixLines(value, " ".repeat(depth)); | ||
} | ||
if (type === "info") { | ||
if (someLogIsError) { | ||
type = "error" | ||
type = "error"; | ||
} else if (someLogIsWarn) { | ||
type = "warn" | ||
type = "warn"; | ||
} | ||
} | ||
logger[type](value) | ||
} | ||
logger[type](value); | ||
}; | ||
const visitRequestNodeToLog = (requestNode, depth) => { | ||
let someLogIsError = false | ||
let someLogIsWarn = false | ||
let someLogIsError = false; | ||
let someLogIsWarn = false; | ||
requestNode.logs.forEach((log) => { | ||
if (log.type === "error") { | ||
someLogIsError = true | ||
someLogIsError = true; | ||
} | ||
if (log.type === "warn") { | ||
someLogIsWarn = true | ||
someLogIsWarn = true; | ||
} | ||
}) | ||
}); | ||
const firstLog = requestNode.logs.shift() | ||
const lastLog = requestNode.logs.pop() | ||
const middleLogs = requestNode.logs | ||
const firstLog = requestNode.logs.shift(); | ||
const lastLog = requestNode.logs.pop(); | ||
const middleLogs = requestNode.logs; | ||
writeLog(firstLog, { someLogIsError, someLogIsWarn, depth }) | ||
writeLog(firstLog, { someLogIsError, someLogIsWarn, depth }); | ||
middleLogs.forEach((log) => { | ||
writeLog(log, { someLogIsError, someLogIsWarn, depth }) | ||
}) | ||
writeLog(log, { someLogIsError, someLogIsWarn, depth }); | ||
}); | ||
requestNode.children.forEach((child) => { | ||
visitRequestNodeToLog(child, depth + 1) | ||
}) | ||
visitRequestNodeToLog(child, depth + 1); | ||
}); | ||
if (lastLog) { | ||
@@ -473,7 +473,7 @@ writeLog(lastLog, { | ||
depth: depth + 1, | ||
}) | ||
}); | ||
} | ||
} | ||
visitRequestNodeToLog(rootRequestNode, 0) | ||
} | ||
}; | ||
visitRequestNodeToLog(rootRequestNode, 0); | ||
}; | ||
nodeRequest.on("error", (error) => { | ||
@@ -486,3 +486,3 @@ if (error.message === "aborted") { | ||
}), | ||
}) | ||
}); | ||
} else { | ||
@@ -495,8 +495,8 @@ // I'm not sure this can happen but it's here in case | ||
}), | ||
}) | ||
}); | ||
} | ||
}) | ||
}); | ||
const pushResponse = async ({ path, method }, { requestNode }) => { | ||
const http2Stream = nodeResponse.stream | ||
const http2Stream = nodeResponse.stream; | ||
@@ -514,8 +514,8 @@ // being able to push a stream is nice to have | ||
), | ||
}) | ||
} | ||
}); | ||
}; | ||
// not aborted, let's try to push a stream into that response | ||
// https://nodejs.org/docs/latest-v16.x/api/http2.html#http2streampushstreamheaders-options-callback | ||
let pushStream | ||
let pushStream; | ||
try { | ||
@@ -534,29 +534,29 @@ pushStream = await new Promise((resolve, reject) => { | ||
if (error) { | ||
reject(error) | ||
reject(error); | ||
} | ||
resolve(pushStream) | ||
resolve(pushStream); | ||
}, | ||
) | ||
}) | ||
); | ||
}); | ||
} catch (e) { | ||
onPushStreamError(e) | ||
return | ||
onPushStreamError(e); | ||
return; | ||
} | ||
const abortController = new AbortController() | ||
const abortController = new AbortController(); | ||
// It's possible to get NGHTTP2_REFUSED_STREAM errors here | ||
// https://github.com/nodejs/node/issues/20824 | ||
const pushErrorCallback = (error) => { | ||
onPushStreamError(error) | ||
abortController.abort() | ||
} | ||
pushStream.on("error", pushErrorCallback) | ||
onPushStreamError(error); | ||
abortController.abort(); | ||
}; | ||
pushStream.on("error", pushErrorCallback); | ||
sendResponseOperation.addEndCallback(() => { | ||
pushStream.removeListener("error", onPushStreamError) | ||
}) | ||
pushStream.removeListener("error", onPushStreamError); | ||
}); | ||
await sendResponseOperation.withSignal(async (signal) => { | ||
const pushResponseOperation = Abort.startOperation() | ||
pushResponseOperation.addAbortSignal(signal) | ||
pushResponseOperation.addAbortSignal(abortController.signal) | ||
const pushResponseOperation = Abort.startOperation(); | ||
pushResponseOperation.addAbortSignal(signal); | ||
pushResponseOperation.addAbortSignal(abortController.signal); | ||
@@ -567,3 +567,3 @@ const pushRequest = createPushRequest(request, { | ||
method, | ||
}) | ||
}); | ||
@@ -573,14 +573,14 @@ try { | ||
requestNode, | ||
}) | ||
}); | ||
if (!abortController.signal.aborted) { | ||
if (pushStream.destroyed) { | ||
abortController.abort() | ||
abortController.abort(); | ||
} else if (!http2Stream.pushAllowed) { | ||
abortController.abort() | ||
abortController.abort(); | ||
} else if (responseProperties.requestAborted) { | ||
} else { | ||
const responseLength = | ||
responseProperties.headers["content-length"] || 0 | ||
responseProperties.headers["content-length"] || 0; | ||
const { effectiveRecvDataLength, remoteWindowSize } = | ||
http2Stream.session.state | ||
http2Stream.session.state; | ||
if ( | ||
@@ -593,4 +593,4 @@ effectiveRecvDataLength + responseLength > | ||
value: `Aborting stream to prevent exceeding remoteWindowSize`, | ||
}) | ||
abortController.abort() | ||
}); | ||
abortController.abort(); | ||
} | ||
@@ -605,13 +605,13 @@ } | ||
responseProperties, | ||
}) | ||
}); | ||
} finally { | ||
await pushResponseOperation.end() | ||
await pushResponseOperation.end(); | ||
} | ||
}) | ||
} | ||
}); | ||
}; | ||
const handleRequest = async (request, { requestNode }) => { | ||
let requestReceivedMeasure | ||
let requestReceivedMeasure; | ||
if (serverTiming) { | ||
requestReceivedMeasure = performance.now() | ||
requestReceivedMeasure = performance.now(); | ||
} | ||
@@ -623,3 +623,3 @@ addRequestLog(requestNode, { | ||
: `${request.method} ${request.url}`, | ||
}) | ||
}); | ||
const warn = (value) => { | ||
@@ -629,6 +629,6 @@ addRequestLog(requestNode, { | ||
value, | ||
}) | ||
} | ||
}); | ||
}; | ||
let requestWaitingTimeout | ||
let requestWaitingTimeout; | ||
if (requestWaitingMs) { | ||
@@ -638,3 +638,3 @@ requestWaitingTimeout = setTimeout( | ||
requestWaitingMs, | ||
).unref() | ||
).unref(); | ||
} | ||
@@ -652,12 +652,12 @@ | ||
...newRequestProperties, | ||
}) | ||
}); | ||
} | ||
}, | ||
) | ||
); | ||
let handleRequestReturnValue | ||
let errorWhileHandlingRequest = null | ||
let handleRequestTimings = serverTiming ? {} : null | ||
let handleRequestReturnValue; | ||
let errorWhileHandlingRequest = null; | ||
let handleRequestTimings = serverTiming ? {} : null; | ||
let timeout | ||
let timeout; | ||
const timeoutPromise = new Promise((resolve) => { | ||
@@ -674,5 +674,5 @@ timeout = setTimeout(() => { | ||
}s waiting to handle request`, | ||
}) | ||
}, responseTimeout) | ||
}) | ||
}); | ||
}, responseTimeout); | ||
}); | ||
const handleRequestPromise = serviceController.callAsyncHooksUntil( | ||
@@ -689,4 +689,4 @@ "handleRequest", | ||
value: `response push ignored because path is invalid (must be a string starting with "/", found ${path})`, | ||
}) | ||
return | ||
}); | ||
return; | ||
} | ||
@@ -697,6 +697,6 @@ if (!request.http2) { | ||
value: `response push ignored because request is not http2`, | ||
}) | ||
return | ||
}); | ||
return; | ||
} | ||
const canPushStream = testCanPushStream(nodeResponse.stream) | ||
const canPushStream = testCanPushStream(nodeResponse.stream); | ||
if (!canPushStream.can) { | ||
@@ -706,10 +706,10 @@ addRequestLog(requestNode, { | ||
value: `response push ignored because ${canPushStream.reason}`, | ||
}) | ||
return | ||
}); | ||
return; | ||
} | ||
let preventedByService = null | ||
let preventedByService = null; | ||
const prevent = () => { | ||
preventedByService = serviceController.getCurrentService() | ||
} | ||
preventedByService = serviceController.getCurrentService(); | ||
}; | ||
serviceController.callHooksUntil( | ||
@@ -724,3 +724,3 @@ "onResponsePush", | ||
() => preventedByService, | ||
) | ||
); | ||
if (preventedByService) { | ||
@@ -730,8 +730,8 @@ addRequestLog(requestNode, { | ||
value: `response push prevented by "${preventedByService.name}" service`, | ||
}) | ||
return | ||
}); | ||
return; | ||
} | ||
const requestChildNode = { logs: [], children: [] } | ||
requestNode.children.push(requestChildNode) | ||
const requestChildNode = { logs: [], children: [] }; | ||
requestNode.children.push(requestChildNode); | ||
await pushResponse( | ||
@@ -743,6 +743,6 @@ { path, method }, | ||
}, | ||
) | ||
); | ||
}, | ||
}, | ||
) | ||
); | ||
try { | ||
@@ -752,9 +752,9 @@ handleRequestReturnValue = await Promise.race([ | ||
handleRequestPromise, | ||
]) | ||
]); | ||
} catch (e) { | ||
errorWhileHandlingRequest = e | ||
errorWhileHandlingRequest = e; | ||
} | ||
clearTimeout(timeout) | ||
clearTimeout(timeout); | ||
let responseProperties | ||
let responseProperties; | ||
if (errorWhileHandlingRequest) { | ||
@@ -765,3 +765,3 @@ if ( | ||
) { | ||
responseProperties = { requestAborted: true } | ||
responseProperties = { requestAborted: true }; | ||
} else { | ||
@@ -777,3 +777,3 @@ // internal error, create 500 response | ||
// il faudrais pouvoir stop que les autres response ? | ||
stop(STOP_REASON_INTERNAL_ERROR) | ||
stop(STOP_REASON_INTERNAL_ERROR); | ||
} | ||
@@ -788,5 +788,5 @@ const handleErrorReturnValue = | ||
}, | ||
) | ||
); | ||
if (!handleErrorReturnValue) { | ||
throw errorWhileHandlingRequest | ||
throw errorWhileHandlingRequest; | ||
} | ||
@@ -801,3 +801,3 @@ addRequestLog(requestNode, { | ||
), | ||
}) | ||
}); | ||
responseProperties = composeTwoResponses( | ||
@@ -814,3 +814,3 @@ { | ||
handleErrorReturnValue, | ||
) | ||
); | ||
} | ||
@@ -825,3 +825,3 @@ } else { | ||
...rest | ||
} = handleRequestReturnValue || {} | ||
} = handleRequestReturnValue || {}; | ||
responseProperties = { | ||
@@ -834,9 +834,9 @@ status, | ||
...rest, | ||
} | ||
}; | ||
} | ||
if (serverTiming) { | ||
const responseReadyMeasure = performance.now() | ||
const responseReadyMeasure = performance.now(); | ||
const timeToStartResponding = | ||
responseReadyMeasure - requestReceivedMeasure | ||
responseReadyMeasure - requestReceivedMeasure; | ||
const serverTiming = { | ||
@@ -846,10 +846,10 @@ ...handleRequestTimings, | ||
"time to start responding": timeToStartResponding, | ||
} | ||
}; | ||
responseProperties.headers = composeTwoHeaders( | ||
responseProperties.headers, | ||
timingToServerTimingResponseHeaders(serverTiming), | ||
) | ||
); | ||
} | ||
if (requestWaitingMs) { | ||
clearTimeout(requestWaitingTimeout) | ||
clearTimeout(requestWaitingTimeout); | ||
} | ||
@@ -864,3 +864,3 @@ if ( | ||
value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`, | ||
}) | ||
}); | ||
} | ||
@@ -879,12 +879,12 @@ serviceController.callHooks( | ||
returnValue, | ||
) | ||
); | ||
} | ||
}, | ||
) | ||
); | ||
serviceController.callHooks("responseReady", responseProperties, { | ||
request, | ||
warn, | ||
}) | ||
return responseProperties | ||
} | ||
}); | ||
return responseProperties; | ||
}; | ||
@@ -902,6 +902,6 @@ const sendResponse = async ({ | ||
// To let a chance to pushed streams we wait a little before sending the response | ||
const ignoreBody = request.method === "HEAD" | ||
const bodyIsEmpty = !responseProperties.body || ignoreBody | ||
const ignoreBody = request.method === "HEAD"; | ||
const bodyIsEmpty = !responseProperties.body || ignoreBody; | ||
if (bodyIsEmpty && requestNode.children.length > 0) { | ||
await new Promise((resolve) => setTimeout(resolve)) | ||
await new Promise((resolve) => setTimeout(resolve)); | ||
} | ||
@@ -916,4 +916,4 @@ | ||
value: `response aborted`, | ||
}) | ||
onRequestHandled(requestNode) | ||
}); | ||
onRequestHandled(requestNode); | ||
}, | ||
@@ -929,7 +929,7 @@ onError: (error) => { | ||
), | ||
}) | ||
onRequestHandled(requestNode) | ||
}); | ||
onRequestHandled(requestNode); | ||
}, | ||
onHeadersSent: ({ status, statusText }) => { | ||
const statusType = statusToType(status) | ||
const statusType = statusToType(status); | ||
addRequestLog(requestNode, { | ||
@@ -949,20 +949,20 @@ type: | ||
}`, | ||
}) | ||
}); | ||
}, | ||
onEnd: () => { | ||
onRequestHandled(requestNode) | ||
onRequestHandled(requestNode); | ||
}, | ||
}) | ||
} | ||
}); | ||
}; | ||
try { | ||
if (receiveRequestOperation.signal.aborted) { | ||
return | ||
return; | ||
} | ||
const responseProperties = await handleRequest(request, { | ||
requestNode: rootRequestNode, | ||
}) | ||
nodeRequest.resume() | ||
}); | ||
nodeRequest.resume(); | ||
if (receiveRequestOperation.signal.aborted) { | ||
return | ||
return; | ||
} | ||
@@ -974,3 +974,3 @@ | ||
if (responseProperties.headers.connection === "keep-alive") { | ||
clearTimeout(request.body.timeout) | ||
clearTimeout(request.body.timeout); | ||
} | ||
@@ -984,10 +984,10 @@ | ||
responseProperties, | ||
}) | ||
}); | ||
} finally { | ||
await sendResponseOperation.end() | ||
await sendResponseOperation.end(); | ||
} | ||
} | ||
const removeRequestListener = listenRequest(nodeServer, requestCallback) | ||
}; | ||
const removeRequestListener = listenRequest(nodeServer, requestCallback); | ||
// ensure we don't try to handle new requests while server is stopping | ||
stopCallbackList.add(removeRequestListener) | ||
stopCallbackList.add(removeRequestListener); | ||
} | ||
@@ -997,17 +997,17 @@ | ||
// https://github.com/websockets/ws/blob/master/doc/ws.md#class-websocket | ||
const websocketHandlers = [] | ||
const websocketHandlers = []; | ||
serviceController.services.forEach((service) => { | ||
const { handleWebsocket } = service | ||
const { handleWebsocket } = service; | ||
if (handleWebsocket) { | ||
websocketHandlers.push(handleWebsocket) | ||
websocketHandlers.push(handleWebsocket); | ||
} | ||
}) | ||
}); | ||
if (websocketHandlers.length > 0) { | ||
const websocketClients = new Set() | ||
const { WebSocketServer } = await import("ws") | ||
let websocketServer = new WebSocketServer({ noServer: true }) | ||
const websocketClients = new Set(); | ||
const { WebSocketServer } = await import("ws"); | ||
let websocketServer = new WebSocketServer({ noServer: true }); | ||
const websocketOrigin = https | ||
? `wss://${hostname}:${port}` | ||
: `ws://${hostname}:${port}` | ||
server.websocketOrigin = websocketOrigin | ||
: `ws://${hostname}:${port}`; | ||
server.websocketOrigin = websocketOrigin; | ||
const upgradeCallback = (nodeRequest, socket, head) => { | ||
@@ -1019,6 +1019,6 @@ websocketServer.handleUpgrade( | ||
async (websocket) => { | ||
websocketClients.add(websocket) | ||
websocketClients.add(websocket); | ||
websocket.once("close", () => { | ||
websocketClients.delete(websocket) | ||
}) | ||
websocketClients.delete(websocket); | ||
}); | ||
const request = fromNodeRequest(nodeRequest, { | ||
@@ -1028,3 +1028,3 @@ serverOrigin: websocketOrigin, | ||
requestBodyLifetime, | ||
}) | ||
}); | ||
serviceController.callAsyncHooksUntil( | ||
@@ -1036,9 +1036,9 @@ "handleWebsocket", | ||
}, | ||
) | ||
); | ||
}, | ||
) | ||
} | ||
); | ||
}; | ||
// see server-polyglot.js, upgrade must be listened on https server when used | ||
const facadeServer = nodeServer._tlsServer || nodeServer | ||
const facadeServer = nodeServer._tlsServer || nodeServer; | ||
const removeUpgradeCallback = listenEvent( | ||
@@ -1048,12 +1048,12 @@ facadeServer, | ||
upgradeCallback, | ||
) | ||
stopCallbackList.add(removeUpgradeCallback) | ||
); | ||
stopCallbackList.add(removeUpgradeCallback); | ||
stopCallbackList.add(() => { | ||
websocketClients.forEach((websocketClient) => { | ||
websocketClient.close() | ||
}) | ||
websocketClients.clear() | ||
websocketServer.close() | ||
websocketServer = null | ||
}) | ||
websocketClient.close(); | ||
}); | ||
websocketClients.clear(); | ||
websocketServer.close(); | ||
websocketServer = null; | ||
}); | ||
} | ||
@@ -1066,5 +1066,5 @@ } | ||
`${serverName} started at ${serverOrigins.local} (${serverOrigins.network})`, | ||
) | ||
); | ||
} else { | ||
logger.info(`${serverName} started at ${serverOrigins.local}`) | ||
logger.info(`${serverName} started at ${serverOrigins.local}`); | ||
} | ||
@@ -1083,10 +1083,10 @@ } | ||
addEffect: (callback) => { | ||
const cleanup = callback() | ||
const cleanup = callback(); | ||
if (typeof cleanup === "function") { | ||
stopCallbackList.add(cleanup) | ||
stopCallbackList.add(cleanup); | ||
} | ||
}, | ||
}) | ||
return server | ||
} | ||
}); | ||
return server; | ||
}; | ||
@@ -1101,3 +1101,3 @@ const createNodeServer = async ({ | ||
if (https) { | ||
const { certificate, privateKey } = https | ||
const { certificate, privateKey } = https; | ||
if (redirectHttpToHttps || allowHttpRequestOnHttps) { | ||
@@ -1109,13 +1109,13 @@ return createPolyglotServer({ | ||
http1Allowed, | ||
}) | ||
}); | ||
} | ||
const { createServer } = await import("node:https") | ||
const { createServer } = await import("node:https"); | ||
return createServer({ | ||
cert: certificate, | ||
key: privateKey, | ||
}) | ||
}); | ||
} | ||
const { createServer } = await import("node:http") | ||
return createServer() | ||
} | ||
const { createServer } = await import("node:http"); | ||
return createServer(); | ||
}; | ||
@@ -1127,3 +1127,3 @@ const testCanPushStream = (http2Stream) => { | ||
reason: `stream.pushAllowed is false`, | ||
} | ||
}; | ||
} | ||
@@ -1133,3 +1133,3 @@ | ||
// And https://github.com/google/node-h2-auto-push/blob/67a36c04cbbd6da7b066a4e8d361c593d38853a4/src/index.ts#L100-L106 | ||
const { remoteWindowSize } = http2Stream.session.state | ||
const { remoteWindowSize } = http2Stream.session.state; | ||
if (remoteWindowSize === 0) { | ||
@@ -1139,3 +1139,3 @@ return { | ||
reason: `no more remoteWindowSize`, | ||
} | ||
}; | ||
} | ||
@@ -1145,4 +1145,4 @@ | ||
can: true, | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -1155,2 +1155,2 @@ const PROCESS_TEARDOWN_EVENTS_MAP = { | ||
exit: STOP_REASON_PROCESS_EXIT, | ||
} | ||
}; |
const createReason = (reasonString) => { | ||
return { | ||
toString: () => reasonString, | ||
} | ||
} | ||
}; | ||
}; | ||
export const STOP_REASON_INTERNAL_ERROR = createReason("Internal error") | ||
export const STOP_REASON_PROCESS_SIGHUP = createReason("process SIGHUP") | ||
export const STOP_REASON_PROCESS_SIGTERM = createReason("process SIGTERM") | ||
export const STOP_REASON_PROCESS_SIGINT = createReason("process SIGINT") | ||
export const STOP_REASON_INTERNAL_ERROR = createReason("Internal error"); | ||
export const STOP_REASON_PROCESS_SIGHUP = createReason("process SIGHUP"); | ||
export const STOP_REASON_PROCESS_SIGTERM = createReason("process SIGTERM"); | ||
export const STOP_REASON_PROCESS_SIGINT = createReason("process SIGINT"); | ||
export const STOP_REASON_PROCESS_BEFORE_EXIT = createReason( | ||
"process before exit", | ||
) | ||
export const STOP_REASON_PROCESS_EXIT = createReason("process exit") | ||
export const STOP_REASON_NOT_SPECIFIED = createReason("not specified") | ||
); | ||
export const STOP_REASON_PROCESS_EXIT = createReason("process exit"); | ||
export const STOP_REASON_NOT_SPECIFIED = createReason("not specified"); |
152760
4793
+ Added@jsenv/log@3.3.5(transitive)
+ Addedansi-escapes@6.2.0(transitive)
+ Addedemoji-regex@10.4.0(transitive)
+ Addedstring-width@6.1.0(transitive)
- Removed@jsenv/log@3.3.4(transitive)
- Removedansi-escapes@6.1.0(transitive)
- Removedemoji-regex@9.2.2(transitive)
- Removedstring-width@5.1.2(transitive)
Updated@jsenv/log@3.3.5