flaska/benchmark/router_v2.mjs

517 lines
18 KiB
JavaScript

const Debug = false
export function printTree(children, indent = 0) {
if (!children.length || !Debug) return
for (let child of children) {
console.log(''.padStart(indent * 2) + (child.char || '*')
+ ' ' + (child.isParams ? `{ params = ${child.isParams} }` : '')
+ (child.isFullParams ? `{ fullParams = ${child.isFullParams} }` : '')
+ ' ' + (child.path ? `{ handler = ${child.path.path} }` : ''))
printTree(child.children, indent + 1)
}
}
class RouterError extends Error {
constructor(route1, route2, ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RouterError);
}
this.name = "RouterError";
this.routeA = route1
this.routeB = route2
}
}
function Child(split, x, i) {
this.path = null
this.isParams = split[x].isParams ? split[x].word : null
this.isFullParams = split[x].isFullParams ? split[x].word : null
this.paramVarName = split[x].paramVarName ?? null
this.char = !this.isParams && !this.isFullParams ? split[x].word[i] || '/' : null
this.count = 0
this.children = []
}
function buildChild(x, i, splitPaths) {
let splitPath = splitPaths[0]
let letter = new Child(splitPath.split, x, i)
let consume = []
if (splitPath.split.length === x + 1
&& (splitPath.split[x].isParams
|| splitPath.split[x].isFullParams
|| splitPath.split[x].word.length === i + 1)) {
letter.path = splitPath.entry
letter.count += 1
} else {
consume = [splitPath]
}
for (let y = 1; y < splitPaths.length; y++) {
let checkPath = splitPaths[y]
if (!checkPath.split[x]
|| checkPath.split[x].isParams !== splitPath.split[x].isParams
|| checkPath.split[x].isFullParams !== splitPath.split[x].isFullParams
|| !checkPath.split[x].isParams
&& !checkPath.split[x].isFullParams
&& (checkPath.split[x].word[i] || '/') !== letter.char) break
consume.push(checkPath)
}
letter.count += consume.length
if (splitPath.split[x].word.length === i || splitPath.split[x].isParams || splitPath.split[x].isFullParams) {
x++
i = -1
}
while (consume.length) {
letter.children.push(buildChild(x, i + 1, consume))
consume.splice(0, letter.children[letter.children.length - 1].count)
}
return letter
}
export function buildTree(splitPaths) {
let builder = []
while (splitPaths.length) {
builder.push(buildChild(0, 0, splitPaths))
splitPaths.splice(0, builder[builder.length - 1].count)
}
return builder
}
const regParamPrefix = /^::?/
const regCleanNonAschii = /(?![a-zA-Z_])./g
const regCleanRest = /_+/g
function splitAndSortPaths(paths, separateStatic = true) {
let staticPaths = new Map()
let paramsPaths = []
let collator = new Intl.Collator('en', { sensitivity: 'accent' });
paths.forEach(function(entry) {
if (entry.path[0] !== '/') throw new RouterError(entry, null, 'Specified route was missing forward slash at start')
// Collect static paths separately
if (entry.path.indexOf('/:') < 0 && separateStatic) {
return staticPaths.set(entry.path, {
path: entry,
params: {}
})
}
// Collect params path separately
paramsPaths.push({
split: entry.path.slice(1).split(/\//g).map(function(word) {
let actualWord = word.replace(regParamPrefix, '')
return {
word: actualWord,
isParams: word[0] === ':' && word[1] !== ':',
isFullParams: word[0] === ':' && word[1] === ':',
paramVarName: word[0] === ':'
? actualWord.replace(regCleanNonAschii, '_').replace(regCleanRest, '_')
: null
}
}),
entry,
})
})
paramsPaths.sort(function(aGroup, bGroup) {
let length = Math.max(aGroup.split.length, bGroup.split.length)
for (let x = 0; x < length; x++) {
let a = aGroup.split[x]
let b = bGroup.split[x]
if (!a) return -1
if (!b) return 1
// Full params go last
if (a.isFullParams && b.isFullParams) throw new RouterError(aGroup.entry, bGroup.entry, 'Two full path routes found on same level')
if (a.isFullParams) return 1
if (b.isFullParams) return -1
// Params go second last
if (a.isParams && !b.isParams) return 1
if (!a.isParams && b.isParams) return -1
// otherwise sort alphabetically if not identical
if (a.word !== b.word) return collator.compare(a.word, b.word)
}
throw new RouterError(aGroup, bGroup, 'Two identical paths were found')
})
return {
staticPaths,
paramsPaths,
}
}
const SlashCode = '/'.charCodeAt(0)
function getIndex(offset, additions, params) {
return (offset + additions)
+ (params.length
? ' + ' + params.map(a => `offset${a[1]}`).join(' + ')
: '')
}
function treeIntoCompiledTreeBufferReturnPath(indentString, paths, branch, params, alt) {
let pathIndex = paths.indexOf(branch.path)
if (pathIndex < 0) {
throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths')
}
let output = ''
output += '\n' + indentString + `return {`
output += '\n' + indentString + ` path: paths[${pathIndex}],`
if (params.length) {
output += '\n' + indentString + ` params: {`
for (let param of params) {
if (alt === 0) {
output += '\n' + indentString + ` ${param[0]}: s${param[1]}.toString(),`
} else if (alt === 1 || alt === 3) {
output += '\n' + indentString + ` ${param[0]}: s${param[1]},`
} else if (alt === 2) {
output += '\n' + indentString + ` ${param[0]}: textDecoder.decode(s${param[1]}),`
}
}
output += '\n' + indentString + ` },`
} else {
output += '\n' + indentString + ` params: {},`
}
output += '\n' + indentString + `}`
return output
}
function treeIntoCompiledTreeBufferBranch(paths, branches, indent = 0, params = [], alt = 0) {
let output = ''
let indentation = ''.padStart((indent - params.length) * 2)
let addEndBracket = true
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
if (i > 0) {
if (!branch.isParams && !branch.isFullParams) {
output += ' else '
} else {
// output += '} //'
output += '\n' + indentation
}
}
if (!branch.isParams && !branch.isFullParams) {
output += `if (buf[${getIndex(indent, 0, params)}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (buf.length === ${getIndex(indent, 1, params)}) {`
output += treeIntoCompiledTreeBufferReturnPath(indentation + ' ', paths, branch, params, alt)
output += '\n' + indentation + ` }`
}
} else {
addEndBracket = false
let paramVarName = (params.length + 1) + '_' + branch.paramVarName
if (alt === 1 || alt === 3) {
output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${getIndex(indent, 0, params)}) >>> 0`})`
} else {
output += `let s${paramVarName} = buf.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${getIndex(indent, 0, params)}) >>> 0`})`
}
output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length`
output += '\n' + indentation
params.push([branch.isParams || branch.isFullParams, paramVarName])
if (branch.isFullParams) {
output += treeIntoCompiledTreeBufferReturnPath(indentation, paths, branch, params, alt)
} else if (branch.path) {
output += '\n' + indentation + `if (buf.length === ${getIndex(indent, 0, params)}) {`
output += treeIntoCompiledTreeBufferReturnPath(indentation + ' ', paths, branch, params, alt)
output += '\n' + indentation + `}`
}
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += treeIntoCompiledTreeBufferBranch(paths, branch.children, indent + 1, params.slice())
}
if (addEndBracket) {
output += '\n' + indentation + '} '
}
}
return output
}
export function treeIntoCompiledTreeBuffer(paths, tree) {
let output = 'return function RBuffer(paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = Buffer.from(str)'
output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
function treeIntoCompiledTreeStringReturnPath(indentString, paths, branch, params) {
let pathIndex = paths.indexOf(branch.path)
if (pathIndex < 0) {
throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths')
}
let output = ''
output += '\n' + indentString + `return {`
output += '\n' + indentString + ` path: paths[${pathIndex}],`
if (params.length) {
output += '\n' + indentString + ` params: {`
for (let param of params) {
output += '\n' + indentString + ` ${param[0]}: s${param[1]},`
}
output += '\n' + indentString + ` },`
} else {
output += '\n' + indentString + ` params: {},`
}
output += '\n' + indentString + `}`
return output
}
function treeIntoCompiledTreeStringBranch(paths, branches, indent = 0, params = []) {
let output = ''
let indentation = ''.padStart((indent - params.length) * 2)
let addEndBracket = true
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
if (i > 0) {
if (!branch.isParams && !branch.isFullParams) {
output += ' else '
} else {
// output += '} //'
output += '\n' + indentation
}
}
if (!branch.isParams && !branch.isFullParams) {
output += `if (str.charCodeAt(${getIndex(indent, 0, params)}) === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (str.length === ${getIndex(indent, 1, params)}) {`
output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + ` }`
}
} else {
addEndBracket = false
let paramVarName = (params.length + 1) + '_' + branch.paramVarName
output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, str.indexOf('/', ${getIndex(indent, 0, params)}) >>> 0`})`
output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length`
output += '\n' + indentation
params.push([branch.isParams || branch.isFullParams, paramVarName])
if (branch.isFullParams) {
output += treeIntoCompiledTreeStringReturnPath(indentation, paths, branch, params)
} else if (branch.path) {
output += '\n' + indentation + `if (str.length === ${getIndex(indent, 0, params)}) {`
output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + `}`
}
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += treeIntoCompiledTreeStringBranch(paths, branch.children, indent + 1, params.slice())
}
if (addEndBracket) {
output += '\n' + indentation + '} '
}
}
return output
}
function treeIntoCompiledTreeStringBranchIndices(paths, branches, indent = 0, params = []) {
let output = ''
let indentation = ''.padStart((indent - params.length) * 2)
let addEndBracket = true
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
if (i > 0) {
if (!branch.isParams && !branch.isFullParams) {
output += ' else '
} else {
// output += '} //'
output += '\n' + indentation
}
}
if (!branch.isParams && !branch.isFullParams) {
output += `if (str[${getIndex(indent, 0, params)}] === '${branch.char}') { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (str.length === ${getIndex(indent, 1, params)}) {`
output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + ` }`
}
} else {
addEndBracket = false
let paramVarName = (params.length + 1) + '_' + branch.paramVarName
output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, str.indexOf('/', ${getIndex(indent, 0, params)}) >>> 0`})`
output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length`
output += '\n' + indentation
params.push([branch.isParams || branch.isFullParams, paramVarName])
if (branch.isFullParams) {
output += treeIntoCompiledTreeStringReturnPath(indentation, paths, branch, params)
} else if (branch.path) {
output += '\n' + indentation + `if (str.length === ${getIndex(indent, 0, params)}) {`
output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + `}`
}
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += treeIntoCompiledTreeStringBranch(paths, branch.children, indent + 1, params.slice())
}
if (addEndBracket) {
output += '\n' + indentation + '} '
}
}
return output
}
export function treeIntoCompiledTreeString(paths, tree) {
let output = 'return function RString(paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n ' + treeIntoCompiledTreeStringBranch(paths, tree, 1)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function treeIntoCompiledTreeStringIndices(paths, tree) {
let output = 'return function RStringIndices(paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n ' + treeIntoCompiledTreeStringBranchIndices(paths, tree, 1)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function treeIntoCompiledTreeBufferAltOne(paths, tree) {
let output = 'return function RBufferStrSlice(paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = Buffer.from(str)'
output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 1)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function treeIntoCompiledTreeBufferAltTwo(paths, tree) {
let output = 'return function RTextEncoderDecoder(textEncoder, textDecoder, paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = textEncoder.encode(str)'
output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 2)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function treeIntoCompiledTreeBufferAltThree(paths, tree) {
let output = 'return function RTextEncoderStrSlice(textEncoder, paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = textEncoder.encode(str)'
output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 3)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function compilePaths(paths) {
let splitPaths = splitAndSortPaths(paths)
let tree = buildTree(splitPaths.paramsPaths.slice())
printTree(tree, 0)
let compiled = treeIntoCompiledTreeBuffer(paths, tree)
let compiledString = treeIntoCompiledTreeString(paths, tree)
let compiledStringIndices = treeIntoCompiledTreeStringIndices(paths, tree)
let compiledTextOne = treeIntoCompiledTreeBufferAltOne(paths, tree)
// let compiledTextTwo = treeIntoCompiledTreeBufferAltTwo(paths, tree)
// let compiledTextThree = treeIntoCompiledTreeBufferAltThree(paths, tree)
return [
[compiled, compiled.bind(null, paths, splitPaths.staticPaths)],
[compiledTextOne, compiledTextOne.bind(null, paths, splitPaths.staticPaths)],
// [compiledTextTwo, compiledTextTwo.bind(null, new TextEncoder(), new TextDecoder(), paths, splitPaths.staticPaths)],
// [compiledTextThree, compiledTextThree.bind(null, new TextEncoder(), paths, splitPaths.staticPaths)],
[compiledString, compiledString.bind(null, paths, splitPaths.staticPaths)],
[compiledStringIndices, compiledStringIndices.bind(null, paths, splitPaths.staticPaths)],
]
}
export function compilePathsBuffer(paths) {
let splitPaths = splitAndSortPaths(paths)
let tree = buildTree(splitPaths.paramsPaths.slice())
printTree(tree, 0)
let compiled = treeIntoCompiledTreeBuffer(paths, tree)
let compiledString = treeIntoCompiledTreeString(paths, tree)
let compiledStringIndices = treeIntoCompiledTreeStringIndices(paths, tree)
let compiledTextOne = treeIntoCompiledTreeBufferAltOne(paths, tree)
// let compiledTextTwo = treeIntoCompiledTreeBufferAltTwo(paths, tree)
// let compiledTextThree = treeIntoCompiledTreeBufferAltThree(paths, tree)
return [
[compiled, compiled.bind(null, paths, splitPaths.staticPaths)],
[compiledTextOne, compiledTextOne.bind(null, paths, splitPaths.staticPaths)],
// [compiledTextTwo, compiledTextTwo.bind(null, new TextEncoder(), new TextDecoder(), paths, splitPaths.staticPaths)],
// [compiledTextThree, compiledTextThree.bind(null, new TextEncoder(), paths, splitPaths.staticPaths)],
[compiledString, compiledString.bind(null, paths, splitPaths.staticPaths)],
[compiledStringIndices, compiledStringIndices.bind(null, paths, splitPaths.staticPaths)],
]
}