EdgeAIG's picture
download
raw
23.8 kB
import * as Router from "../index.js"
import * as QS from "../QueryString.js"
const FULL_PATH_REGEXP = /^https?:\/\/.*?\//
const OPTIONAL_PARAM_REGEXP = /(\/:[^/()]*?)\?(\/?)/
interface Route<A = unknown> {
readonly method: string
readonly path: Router.PathInput
readonly pattern: string
readonly handler: A
readonly params: ReadonlyArray<string>
}
/** @internal */
export const make = <A>(
options: Partial<Router.RouterConfig> = {},
): Router.Router<A> => new RouterImpl(options)
class RouterImpl<A> implements Router.Router<A> {
constructor(options: Partial<Router.RouterConfig> = {}) {
this.options = {
ignoreTrailingSlash: true,
ignoreDuplicateSlashes: true,
caseSensitive: false,
maxParamLength: 100,
...options,
}
}
readonly options: Router.RouterConfig
routes: Array<Route> = []
trees: Record<string, StaticNode> = {}
on(
method: string | Iterable<string>,
path: Router.PathInput,
handler: A,
): void {
const optionalParamMatch = path.match(OPTIONAL_PARAM_REGEXP)
if (optionalParamMatch && optionalParamMatch.index !== undefined) {
assert(
path.length === optionalParamMatch.index + optionalParamMatch[0].length,
"Optional Parameter needs to be the last parameter of the path",
)
const pathFull = path.replace(
OPTIONAL_PARAM_REGEXP,
"$1$2",
) as Router.PathInput
const pathOptional = path.replace(
OPTIONAL_PARAM_REGEXP,
"$2",
) as Router.PathInput
this.on(method, pathFull, handler)
this.on(method, pathOptional, handler)
return
}
if (this.options.ignoreDuplicateSlashes) {
path = removeDuplicateSlashes(path)
}
if (this.options.ignoreTrailingSlash) {
path = trimLastSlash(path)
}
const methods = typeof method === "string" ? [method] : method
for (const method of methods) {
this._on(method, path, handler)
}
}
all(path: Router.PathInput, handler: A) {
this.on(httpMethods, path, handler)
}
private _on(method: string, path: Router.PathInput, handler: A): void {
if (this.trees[method] === undefined) {
this.trees[method] = new StaticNode("/")
}
let pattern = path
if (pattern === "*" && this.trees[method].prefix.length !== 0) {
const currentRoot = this.trees[method]
this.trees[method] = new StaticNode("")
this.trees[method].staticChildren["/"] = currentRoot
}
let parentNodePathIndex = this.trees[method].prefix.length
let currentNode: Node = this.trees[method]
const params = []
for (let i = 0; i <= pattern.length; i++) {
if (pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) === 58) {
// It's a double colon
i++
continue
}
const isParametricNode =
pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58
const isWildcardNode = pattern.charCodeAt(i) === 42
if (
isParametricNode ||
isWildcardNode ||
(i === pattern.length && i !== parentNodePathIndex)
) {
let staticNodePath = pattern.slice(parentNodePathIndex, i)
if (!this.options.caseSensitive) {
staticNodePath = staticNodePath.toLowerCase()
}
staticNodePath = staticNodePath.split("::").join(":")
staticNodePath = staticNodePath.split("%").join("%25")
// add the static part of the route to the tree
currentNode = (currentNode as StaticNode).createStaticChild(
staticNodePath,
)
}
if (isParametricNode) {
let isRegexNode = false
const regexps = []
let lastParamStartIndex = i + 1
for (let j = lastParamStartIndex; ; j++) {
const charCode = pattern.charCodeAt(j)
const isRegexParam = charCode === 40
const isStaticPart = charCode === 45 || charCode === 46
const isEndOfNode = charCode === 47 || j === pattern.length
if (isRegexParam || isStaticPart || isEndOfNode) {
const paramName = pattern.slice(lastParamStartIndex, j)
params.push(paramName)
isRegexNode = isRegexNode || isRegexParam || isStaticPart
if (isRegexParam) {
const endOfRegexIndex = getClosingParenthensePosition(pattern, j)
const regexString = pattern.slice(j, endOfRegexIndex + 1)
regexps.push(trimRegExpStartAndEnd(regexString))
j = endOfRegexIndex + 1
} else {
regexps.push("(.*?)")
}
const staticPartStartIndex = j
for (; j < pattern.length; j++) {
const charCode = pattern.charCodeAt(j)
if (charCode === 47) break
if (charCode === 58) {
const nextCharCode = pattern.charCodeAt(j + 1)
if (nextCharCode === 58) j++
else break
}
}
let staticPart = pattern.slice(staticPartStartIndex, j)
if (staticPart) {
staticPart = staticPart.split("::").join(":")
staticPart = staticPart.split("%").join("%25")
regexps.push(escapeRegExp(staticPart))
}
lastParamStartIndex = j + 1
if (
isEndOfNode ||
pattern.charCodeAt(j) === 47 ||
j === pattern.length
) {
const nodePattern = isRegexNode ? "()" + staticPart : staticPart
const nodePath = pattern.slice(i, j)
pattern = pattern.slice(0, i + 1) + nodePattern + pattern.slice(j)
i += nodePattern.length
const regex = isRegexNode
? new RegExp("^" + regexps.join("") + "$")
: undefined
currentNode = (currentNode as StaticNode).createParametricChild(
regex,
staticPart,
nodePath,
)
parentNodePathIndex = i + 1
break
}
}
}
} else if (isWildcardNode) {
// add the wildcard parameter
params.push("*")
currentNode = (currentNode as StaticNode).createWildcardChild()
parentNodePathIndex = i + 1
if (i !== pattern.length - 1) {
throw new Error("Wildcard must be the last character in the route")
}
}
}
if (!this.options.caseSensitive) {
pattern = pattern.toLowerCase() as Router.PathInput
}
if (pattern === "*") {
pattern = "/*"
}
for (const existRoute of this.routes) {
if (existRoute.method === method && existRoute.pattern === pattern) {
throw new Error(
`Method '${method}' already declared for route '${pattern}'`,
)
}
}
const route = { method, path, pattern, params, handler }
this.routes.push(route)
currentNode.addRoute(route)
}
has(method: string, path: string): boolean {
const node = this.trees[method]
if (node === undefined) {
return false
}
const staticNode = node.getStaticChild(path)
if (staticNode === undefined) {
return false
}
return staticNode.isLeafNode
}
find(method: string, path: string): Router.FindResult<A> | undefined {
let currentNode: Node | undefined = this.trees[method]
if (currentNode === undefined) return undefined
if (path.charCodeAt(0) !== 47) {
// 47 is '/'
path = path.replace(FULL_PATH_REGEXP, "/")
}
// This must be run before sanitizeUrl as the resulting function
// .sliceParameter must be constructed with same URL string used
// throughout the rest of this function.
if (this.options.ignoreDuplicateSlashes) {
path = removeDuplicateSlashes(path)
}
let sanitizedUrl
let querystring
let shouldDecodeParam
try {
sanitizedUrl = safeDecodeURI(path)
path = sanitizedUrl.path
querystring = sanitizedUrl.querystring
shouldDecodeParam = sanitizedUrl.shouldDecodeParam
} catch (error) {
return undefined
}
if (this.options.ignoreTrailingSlash) {
path = trimLastSlash(path)
}
const originPath = path
if (this.options.caseSensitive === false) {
path = path.toLowerCase()
}
const maxParamLength = this.options.maxParamLength
let pathIndex = (currentNode as StaticNode).prefix.length
const params = []
const pathLen = path.length
const brothersNodesStack: Array<BrotherNode> = []
while (true) {
if (pathIndex === pathLen && currentNode.isLeafNode) {
const handle = currentNode.handlerStorage?.find()
if (handle !== undefined) {
return {
handler: handle.handler as A,
params: handle.createParams(params),
searchParams: QS.parse(querystring),
} as const
}
}
let node: Node | undefined = currentNode.getNextNode(
path,
pathIndex,
brothersNodesStack,
params.length,
)
if (node === undefined) {
if (brothersNodesStack.length === 0) {
return undefined
}
const brotherNodeState = brothersNodesStack.pop()!
pathIndex = brotherNodeState.brotherPathIndex
params.splice(brotherNodeState.paramsCount)
node = brotherNodeState.brotherNode
}
currentNode = node
// static route
if (currentNode._tag === "StaticNode") {
pathIndex += currentNode.prefix.length
continue
}
if (currentNode._tag === "WildcardNode") {
let param = originPath.slice(pathIndex)
if (shouldDecodeParam) {
param = safeDecodeURIComponent(param)
}
params.push(param)
pathIndex = pathLen
continue
}
if (currentNode._tag === "ParametricNode") {
let paramEndIndex = originPath.indexOf("/", pathIndex)
if (paramEndIndex === -1) {
paramEndIndex = pathLen
}
let param = originPath.slice(pathIndex, paramEndIndex)
if (shouldDecodeParam) {
param = safeDecodeURIComponent(param)
}
if (currentNode.regex !== undefined) {
const matchedParameters: RegExpExecArray | null =
currentNode.regex.exec(param)
if (matchedParameters === null) continue
for (let i = 1; i < matchedParameters.length; i++) {
const matchedParam: string = matchedParameters[i]
if (matchedParam.length > maxParamLength) {
return undefined
}
params.push(matchedParam)
}
} else {
if (param.length > maxParamLength) {
return undefined
}
params.push(param)
}
pathIndex = paramEndIndex
}
}
}
}
// ----------------------------------------------------------------------------
// Handler storage
// ----------------------------------------------------------------------------
interface Handler {
readonly params: ReadonlyArray<string>
readonly handler: unknown
readonly createParams: (
paramsArray: ReadonlyArray<string>,
) => Record<string, string>
}
class HandlerStorage {
readonly handlers: Array<Handler> = []
unconstrainedHandler: Handler | undefined
find() {
return this.unconstrainedHandler
}
add(route: Route) {
const handler: Handler = {
params: route.params,
handler: route.handler,
createParams: compileCreateParams(route.params),
}
this.handlers.push(handler)
this.unconstrainedHandler = this.handlers[0]
}
}
// ----------------------------------------------------------------------------
// Nodes
// ----------------------------------------------------------------------------
interface BrotherNode {
readonly paramsCount: number
readonly brotherPathIndex: number
readonly brotherNode: Node
}
type Node = StaticNode | ParametricNode | WildcardNode
abstract class NodeBase {
isLeafNode = false
routes: Array<Route> | undefined
handlerStorage: HandlerStorage | undefined
addRoute(route: Route) {
if (this.routes === undefined) {
this.routes = [route]
} else {
this.routes.push(route)
}
if (this.handlerStorage === undefined) {
this.handlerStorage = new HandlerStorage()
}
this.isLeafNode = true
this.handlerStorage.add(route)
}
abstract getNextNode(
path: string,
pathIndex: number,
nodeStack: any,
paramsCount: number,
): Node | undefined
}
abstract class ParentNode extends NodeBase {
readonly staticChildren: Record<string, StaticNode> = {}
findStaticMatchingChild(
path: string,
pathIndex: number,
): StaticNode | undefined {
const staticChild = this.staticChildren[path.charAt(pathIndex)]
if (
staticChild === undefined ||
!staticChild.matchPrefix(path, pathIndex)
) {
return undefined
}
return staticChild
}
getStaticChild(path: string, pathIndex = 0): StaticNode | undefined {
if (path.length === pathIndex) {
return this as any
}
const staticChild = this.findStaticMatchingChild(path, pathIndex)
if (staticChild === undefined) {
return undefined
}
return staticChild.getStaticChild(
path,
pathIndex + staticChild.prefix.length,
)
}
createStaticChild(path: string): StaticNode {
if (path.length === 0) {
return this as any
}
let staticChild = this.staticChildren[path.charAt(0)]
if (staticChild) {
let i = 1
for (; i < staticChild.prefix.length; i++) {
if (path.charCodeAt(i) !== staticChild.prefix.charCodeAt(i)) {
staticChild = staticChild.split(this, i)
break
}
}
return staticChild.createStaticChild(path.slice(i))
}
const label = path.charAt(0)
this.staticChildren[label] = new StaticNode(path)
return this.staticChildren[label]
}
}
class StaticNode extends ParentNode {
readonly _tag = "StaticNode"
constructor(prefix: string) {
super()
this.setPrefix(prefix)
}
prefix!: string
matchPrefix!: (path: string, pathIndex: number) => boolean
readonly parametricChildren: Array<ParametricNode> = []
wildcardChild: WildcardNode | undefined
private setPrefix(prefix: string) {
this.prefix = prefix
if (prefix.length === 1) {
this.matchPrefix = (_path, _pathIndex) => true
} else {
const len = prefix.length
this.matchPrefix = function (path, pathIndex) {
for (let i = 1; i < len; i++) {
if (path.charCodeAt(pathIndex + i) !== this.prefix.charCodeAt(i)) {
return false
}
}
return true
}
}
}
getParametricChild(regex: RegExp | undefined): ParametricNode | undefined {
if (regex === undefined) {
return this.parametricChildren.find(child => child.isRegex === false)
}
const source = regex.source
return this.parametricChildren.find(child => {
if (child.regex === undefined) {
return false
}
return child.regex.source === source
})
}
createParametricChild(
regex: RegExp | undefined,
staticSuffix: string | undefined,
nodePath: string,
) {
let child = this.getParametricChild(regex)
if (child !== undefined) {
child.nodePaths.add(nodePath)
return child
}
child = new ParametricNode(regex, staticSuffix, nodePath)
this.parametricChildren.push(child)
this.parametricChildren.sort((child1, child2) => {
if (!child1.isRegex) return 1
if (!child2.isRegex) return -1
if (child1.staticSuffix === undefined) return 1
if (child2.staticSuffix === undefined) return -1
if (child2.staticSuffix.endsWith(child1.staticSuffix)) return 1
if (child1.staticSuffix.endsWith(child2.staticSuffix)) return -1
return 0
})
return child
}
createWildcardChild() {
if (this.wildcardChild === undefined) {
this.wildcardChild = new WildcardNode()
}
return this.wildcardChild
}
split(parentNode: ParentNode, length: number) {
const parentPrefix = this.prefix.slice(0, length)
const childPrefix = this.prefix.slice(length)
this.setPrefix(childPrefix)
const staticNode = new StaticNode(parentPrefix)
staticNode.staticChildren[childPrefix.charAt(0)] = this
parentNode.staticChildren[parentPrefix.charAt(0)] = staticNode
return staticNode
}
getNextNode(
path: string,
pathIndex: number,
nodeStack: Array<BrotherNode>,
paramsCount: number,
): Node | undefined {
let node: Node | undefined = this.findStaticMatchingChild(path, pathIndex)
let parametricBrotherNodeIndex = 0
if (node === undefined) {
if (this.parametricChildren.length === 0) {
return this.wildcardChild
}
node = this.parametricChildren[0]
parametricBrotherNodeIndex = 1
}
if (this.wildcardChild !== undefined) {
nodeStack.push({
paramsCount,
brotherPathIndex: pathIndex,
brotherNode: this.wildcardChild,
})
}
for (
let i = this.parametricChildren.length - 1;
i >= parametricBrotherNodeIndex;
i--
) {
nodeStack.push({
paramsCount,
brotherPathIndex: pathIndex,
brotherNode: this.parametricChildren[i],
})
}
return node
}
}
class ParametricNode extends ParentNode {
readonly _tag = "ParametricNode"
constructor(
readonly regex: RegExp | undefined,
readonly staticSuffix: string | undefined,
nodePath: string,
) {
super()
this.isRegex = !!regex
this.nodePaths = new Set([nodePath])
}
readonly isRegex: boolean
readonly nodePaths: Set<string>
getNextNode(path: string, pathIndex: number) {
return this.findStaticMatchingChild(path, pathIndex)
}
}
class WildcardNode extends NodeBase {
readonly _tag = "WildcardNode"
getNextNode(
_path: string,
_pathIndex: number,
_nodeStack: any,
_paramsCount: number,
): Node | undefined {
return undefined
}
}
// --
interface Assert {
(condition: any, message?: string): asserts condition
}
const assert: Assert = (condition, message) => {
if (!condition) {
throw new Error(message)
}
}
function removeDuplicateSlashes(path: string): Router.PathInput {
return path.replace(/\/\/+/g, "/") as Router.PathInput
}
function trimLastSlash(path: string): Router.PathInput {
if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
return path.slice(0, -1) as Router.PathInput
}
return path as Router.PathInput
}
function compileCreateParams(
params: ReadonlyArray<string>,
): (paramsArray: ReadonlyArray<string>) => Record<string, string> {
const len = params.length
return function (paramsArray) {
const paramsObject: Record<string, string> = {}
for (let i = 0; i < len; i++) {
paramsObject[params[i]] = paramsArray[i]
}
return paramsObject
}
}
function getClosingParenthensePosition(path: string, idx: number) {
// `path.indexOf()` will always return the first position of the closing parenthese,
// but it's inefficient for grouped or wrong regexp expressions.
// see issues #62 and #63 for more info
let parentheses = 1
while (idx < path.length) {
idx++
// ignore skipped chars
if (path[idx] === "\\") {
idx++
continue
}
if (path[idx] === ")") {
parentheses--
} else if (path[idx] === "(") {
parentheses++
}
if (!parentheses) return idx
}
throw new TypeError('Invalid regexp expression in "' + path + '"')
}
function trimRegExpStartAndEnd(regexString: string) {
// removes chars that marks start "^" and end "$" of regexp
if (regexString.charCodeAt(1) === 94) {
regexString = regexString.slice(0, 1) + regexString.slice(2)
}
if (regexString.charCodeAt(regexString.length - 2) === 36) {
regexString =
regexString.slice(0, regexString.length - 2) +
regexString.slice(regexString.length - 1)
}
return regexString
}
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
// It must spot all the chars where decodeURIComponent(x) !== decodeURI(x)
// The chars are: # $ & + , / : ; = ? @
function decodeComponentChar(highCharCode: number, lowCharCode: number) {
if (highCharCode === 50) {
if (lowCharCode === 53) return "%"
if (lowCharCode === 51) return "#"
if (lowCharCode === 52) return "$"
if (lowCharCode === 54) return "&"
if (lowCharCode === 66) return "+"
if (lowCharCode === 98) return "+"
if (lowCharCode === 67) return ","
if (lowCharCode === 99) return ","
if (lowCharCode === 70) return "/"
if (lowCharCode === 102) return "/"
return undefined
}
if (highCharCode === 51) {
if (lowCharCode === 65) return ":"
if (lowCharCode === 97) return ":"
if (lowCharCode === 66) return ";"
if (lowCharCode === 98) return ";"
if (lowCharCode === 68) return "="
if (lowCharCode === 100) return "="
if (lowCharCode === 70) return "?"
if (lowCharCode === 102) return "?"
return undefined
}
if (highCharCode === 52 && lowCharCode === 48) {
return "@"
}
return undefined
}
function safeDecodeURI(path: string) {
let shouldDecode = false
let shouldDecodeParam = false
let querystring = ""
for (let i = 1; i < path.length; i++) {
const charCode = path.charCodeAt(i)
if (charCode === 37) {
const highCharCode = path.charCodeAt(i + 1)
const lowCharCode = path.charCodeAt(i + 2)
if (decodeComponentChar(highCharCode, lowCharCode) === undefined) {
shouldDecode = true
} else {
shouldDecodeParam = true
// %25 - encoded % char. We need to encode one more time to prevent double decoding
if (highCharCode === 50 && lowCharCode === 53) {
shouldDecode = true
path = path.slice(0, i + 1) + "25" + path.slice(i + 1)
i += 2
}
i += 2
}
// Some systems do not follow RFC and separate the path and query
// string with a `;` character (code 59), e.g. `/foo;jsessionid=123456`.
// Thus, we need to split on `;` as well as `?` and `#`.
} else if (charCode === 63 || charCode === 59 || charCode === 35) {
querystring = path.slice(i + 1)
path = path.slice(0, i)
break
}
}
const decodedPath = shouldDecode ? decodeURI(path) : path
return { path: decodedPath, querystring, shouldDecodeParam } as const
}
function safeDecodeURIComponent(uriComponent: string) {
const startIndex = uriComponent.indexOf("%")
if (startIndex === -1) return uriComponent
let decoded = ""
let lastIndex = startIndex
for (let i = startIndex; i < uriComponent.length; i++) {
if (uriComponent.charCodeAt(i) === 37) {
const highCharCode = uriComponent.charCodeAt(i + 1)
const lowCharCode = uriComponent.charCodeAt(i + 2)
const decodedChar = decodeComponentChar(highCharCode, lowCharCode)
decoded += uriComponent.slice(lastIndex, i) + decodedChar
lastIndex = i + 3
}
}
return (
uriComponent.slice(0, startIndex) + decoded + uriComponent.slice(lastIndex)
)
}
const httpMethods = [
"ACL",
"BIND",
"CHECKOUT",
"CONNECT",
"COPY",
"DELETE",
"GET",
"HEAD",
"LINK",
"LOCK",
"M-SEARCH",
"MERGE",
"MKACTIVITY",
"MKCALENDAR",
"MKCOL",
"MOVE",
"NOTIFY",
"OPTIONS",
"PATCH",
"POST",
"PROPFIND",
"PROPPATCH",
"PURGE",
"PUT",
"REBIND",
"REPORT",
"SEARCH",
"SOURCE",
"SUBSCRIBE",
"TRACE",
"UNBIND",
"UNLINK",
"UNLOCK",
"UNSUBSCRIBE",
] as const

Xet Storage Details

Size:
23.8 kB
·
Xet hash:
82c902dd631b0147e58062d1d3d25d72596c2c15b437512f74e3b21ee280e3c1

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.