Compare commits

...

8 commits

Author SHA1 Message Date
d031efb89f Add missing built-in middleware CorsHandler to readme
All checks were successful
/ deploy (push) Successful in 17s
2024-11-10 02:08:13 +00:00
bed505fbe2 Switch to pnpm
All checks were successful
/ deploy (push) Successful in 18s
2024-11-10 01:43:01 +00:00
479f98a367 add forgejo workflow and deploy v1.4.0
All checks were successful
/ deploy (push) Successful in 19s
2024-11-10 01:35:05 +00:00
ee0326c9f4 remove all the nonsense benchmark stuff 2024-11-10 00:26:27 +00:00
566c59ec95 more benchmark nonsense 2024-11-10 00:24:14 +00:00
f48d63038d buffer and charCodeAt impl 2024-11-02 23:40:54 +00:00
2e1dadbdea benchmark nonsense 2024-11-02 12:45:19 +00:00
8db66f416a Minor refactor of tests and eltro
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
2024-09-29 05:02:39 +00:00
33 changed files with 2631 additions and 1891 deletions

View file

@ -0,0 +1,44 @@
on:
push:
branches:
- master
jobs:
deploy:
runs-on: arch
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Install dependencies
run: time pnpm install
- name: Run Tests
run: pnpm run test --ignore-only
- name: Deply if new version
run: |
echo ""
echo "------------------------------------"
echo ""
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases for name ${CURR_NAME}"
if curl -s -X GET -H "Authorization: token ${{ secrets.deploytoken }}" https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases | grep -o "\"name\":\"${CURR_NAME}\"" > /dev/null; then
echo "Skipping ${{ github.job }} since $CURR_NAME already exists";
exit;
fi
echo "New release ${CURR_VER} found, beginning deployment"
echo "Creating ${CURR_VER} release on forgejo"
curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases \
-d "{\"tag_name\":\"${CURR_VER}\",\"name\":\"${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${{ github.sha }} :\n\n${{ github.event.head_commit.message }}\"}" | jq
echo "//registry.npmjs.org/:_authToken=${{ secrets.npmtoken }}" > ~/.npmrc
echo "Publishing new version to npm"
npm publish

View file

@ -189,6 +189,10 @@ Parse the search query and create a map with key->value in `ctx.query`.
Parse incoming request body as json and store it in `ctx.req.body`. Parse incoming request body as json and store it in `ctx.req.body`.
* `CorsHandler()`
A secure implementation for handling CORS requests.
* `FormidableHandler()` * `FormidableHandler()`
Provides a wrapper to handle an incoming file upload using `Formidable@1`. Provides a wrapper to handle an incoming file upload using `Formidable@1`.

View file

@ -0,0 +1,116 @@
import { printTree } from "./utils.mjs"
/**
* Child {
* char: 'a',
* children: [Child, Child],
* path: null / 'full/path',
* count: 0
* }
*/
function Child(char) {
this.char = char
this.children = []
this.path = null
this.count = 0
}
function buildChild(i, arr) {
let letter = new Child(arr[0][i])
let consume = []
if (arr[0].length === i + 1) {
letter.path = arr[0]
letter.count += 1
} else {
consume = [arr[0]]
}
for (let y = 1; y < arr.length; y++) {
if (arr[y][i] !== letter.char) break
consume.push(arr[y])
}
letter.count += consume.length
while (consume.length) {
letter.children.push(buildChild(i + 1, consume))
consume.splice(0, letter.children[letter.children.length - 1].count)
}
return letter
}
export function buildTree(all) {
let paths = Array.from(new Set(all)).sort()
let builder = []
while (paths.length) {
builder.push(buildChild(0, paths))
paths.splice(0, builder[builder.length - 1].count)
}
printTree(builder)
return builder
}
function IfTreeBranch(branches, indent = 0) {
let output = ''
let indentation = ''.padStart(indent * 2)
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
output += `${i > 0 ? 'else ': ''}if (str.charCodeAt(${indent}) === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (str.length === ${branch.path.length}) {`
output += '\n' + indentation + ` return "${branch.path}"`
output += '\n' + indentation + ` }`
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += IfTreeBranch(branch.children, indent + 1)
}
output += '\n' + indentation + '} '
}
return output
}
export function compileTreeIntoIfs(tree) {
let output = IfTreeBranch(tree)
output += '\nreturn null'
return new Function('str', output)
}
function IfTreeBranchBuffer(branches, indent = 0) {
let output = ''
let indentation = ''.padStart(indent * 2)
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
output += `${i > 0 ? 'else ': ''}if (uint[${indent}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (uint.length === ${branch.path.length}) {`
output += '\n' + indentation + ` return "${branch.path}"`
output += '\n' + indentation + ` }`
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += IfTreeBranchBuffer(branch.children, indent + 1)
}
output += '\n' + indentation + '} '
}
return output
}
export function compileTreeIntoIfsWithBuffer(tree) {
let output = `var uint = Buffer.from(str)\n`
output += IfTreeBranchBuffer(tree)
output += '\nreturn null'
return new Function('str', output)
}

View file

@ -0,0 +1,39 @@
export function printTree(children, indent = 0) {
if (!children.length) return
for (let child of children) {
console.log(child.char.padStart(1 + indent * 2))
printTree(child.children, indent + 1)
}
}
export function printIfNotEightyOne(fn) {
let opt = %GetOptimizationStatus(fn)
if (opt === 81) return
printCurrentStatus()
}
export function printCurrentStatus(fn) {
let opt = %GetOptimizationStatus(fn)
console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt})`)
}
export function printStatusHelperText() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned
lite mode
marked for deoptimization
baseline
topmost frame is interpreted
topmost frame is baseline`)
}

View file

@ -6,6 +6,15 @@ export const overrideDummy = function(override) {
export const dummy = function() { callItem() } export const dummy = function() { callItem() }
export const testRoutes = [
// '/api/articles/:id/file/:fileId',
'/',
// '/api/articles',
// '/api/articles/:id/file',
// '/api/articles/:id',
'/::rest',
]
export const allRoutes = [ export const allRoutes = [
'/', '/',
'/api/articles', '/api/articles',
@ -23,6 +32,22 @@ export const allRoutes = [
'/api/pages/:id/articles/public', '/api/pages/:id/articles/public',
'/api/staff', '/api/staff',
'/api/staff/:id', '/api/staff/:id',
'/::rest',
]
export const allRoutesGoldenBalance = [
'/',
'/a/:a/aaaaaaaaaaaaaaaaaaaaaaaaaaaaa/:aa',
'/b/:b/bbbbbbbbbbbbbbbbbbbbbbbbbbbbb/:bb',
'/c/:c/ccccccccccccccccccccccccccccc/:cc',
'/d/:d/ddddddddddddddddddddddddddddd/:dd',
'/e/:e/eeeeeeeeeeeeeeeeeeeeeeeeeeeee/:ee',
'/f/:f/fffffffffffffffffffffffffffff/:ff',
'/g/:g/ggggggggggggggggggggggggggggg/:gg',
'/h/:h/hhhhhhhhhhhhhhhhhhhhhhhhhhhhh/:hh',
'/i/:i/iiiiiiiiiiiiiiiiiiiiiiiiiiiii/:ii',
'/j/:j/jjjjjjjjjjjjjjjjjjjjjjjjjjjjj/:jj',
'/::rest',
] ]
export const allManyRoutes = [ export const allManyRoutes = [
@ -37,7 +62,7 @@ export const allManyRoutes = [
'/api/categories/:categoryId/properties', '/api/categories/:categoryId/properties',
'/api/categories/:categoryId/values/:props', '/api/categories/:categoryId/values/:props',
'/api/categories/:categoryId', '/api/categories/:categoryId',
'/api/categories/:categoryId/products/:productId', //'/api/categories/:categoryId/products/:productId',
'/api/categories/:categoryId/products/:productId', '/api/categories/:categoryId/products/:productId',
'/api/customers', '/api/customers',
'/api/customers/:id', '/api/customers/:id',
@ -61,7 +86,7 @@ export const allManyRoutes = [
'/api/products/:id', '/api/products/:id',
'/api/products/:id/movement', '/api/products/:id/movement',
'/api/products/:id/sub_products/:productId', '/api/products/:id/sub_products/:productId',
'/api/products/:id/sub_products/:productId', //'/api/products/:id/sub_products/:productId',
'/api/products/code/:code', '/api/products/code/:code',
'/api/products/property/:propertyId', '/api/products/property/:propertyId',
'/api/properties', '/api/properties',
@ -84,4 +109,5 @@ export const allManyRoutes = [
'/api/works/public', '/api/works/public',
'/api/staff', '/api/staff',
'/api/staff/:id', '/api/staff/:id',
'/::rest',
] ]

148
benchmark/ifs.mjs Normal file
View file

@ -0,0 +1,148 @@
import { summary, run, bench } from 'mitata';
function printCurrentStatus(fn) {
let opt = %GetOptimizationStatus(fn)
console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`)
}
function printStatusHelperText() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned
lite mode
marked for deoptimization
baseline
topmost frame is interpreted
topmost frame is baseline`)
}
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
function ifSingle(a) {
if (a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) {
return 100
}
return 10
}
function ifChain(a) {
if (a[0] === 97)
if (a[1] === 97)
if (a[2] === 97)
if (a[3] === 97)
if (a.length === 4) {
return 100
}
return 10
}
function ifSingleOpt(a) {
if (a.length >= 4 && a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) {
return 100
}
return 10
}
function ifChainOpt(a) {
if (a.length >= 4)
if (a[0] === 97)
if (a[1] === 97)
if (a[2] === 97)
if (a[3] === 97)
if (a.length === 4) {
return 100
}
return 10
}
function ifSingleOptAlt(a) {
if (a.length >= 4 && a[0] == 97 && a[1] == 97 && a[2] == 97 && a[3] == 97 && a.length == 4) {
return 100
}
return 10
}
function ifChainOptAlt(a) {
if (a.length >= 4)
if (a[0] == 97)
if (a[1] == 97)
if (a[2] == 97)
if (a[3] == 97)
if (a.length == 4) {
return 100
}
return 10
}
let paths = [
[97, 97, 97, 97],
[97, 97, 97, 97, 97],
[97, 97, 96, 97],
[97, 96, 97, 97],
[96, 97, 97, 97],
[],
[98],
[97, 97, 97],
[97, 97],
[97],
[97, 97, 96],
[97, 96],
[96],
]
let paths2 = [
[97, 97, 97, 97],
[97, 97, 97, 97, 97],
[97, 97, 96, 97],
[97, 96, 97, 97],
[96, 97, 97, 97],
[97, 97, 97, 97],
[97, 97, 97, 97, 97],
[97, 97, 96, 97],
[97, 96, 97, 97],
[96, 97, 97, 97],
[97, 97, 97, 97, 97, 97],
[97, 97, 97, 97, 96, 97],
[97, 97, 97, 97, 97, 96],
]
let func1 = [ifSingle, ifChain, ifSingleOpt, ifChainOpt, ifSingleOptAlt, ifChainOptAlt];
for (let fun of func1) {
console.log('-- begin', fun.name)
for (var i = 0; i < 1000000; i++) {
paths.map(fun)
}
printCurrentStatus(fun);
}
printStatusHelperText()
summary(() => {
func1.forEach(function(fun) {
bench(fun.name + ' first', function() {
return paths.map(fun)
})
})
func1.forEach(function(fun) {
bench(fun.name + ' second', function() {
return paths2.map(fun)
})
})
})
run().then(function() {
for (let fun of func1) {
printCurrentStatus(fun);
}
printStatusHelperText()
});

85
benchmark/ifs_compile.mjs Normal file
View file

@ -0,0 +1,85 @@
import { printCurrentStatus, printStatusHelperText } from "./compiler/utils.mjs";
function ifSingle(a) {
if (a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) {
return 100
}
return 10
}
function ifChain(a) {
if (a[0] === 97)
if (a[1] === 97)
if (a[2] === 97)
if (a[3] === 97)
if (a.length === 4) {
return 100
}
return 10
}
function ifSingleOptimized(a) {
if (a.length >= 4 && a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) {
return 100
}
return 10
}
function ifChainOptimized(a) {
if (a.length >= 4)
if (a[0] === 97)
if (a[1] === 97)
if (a[2] === 97)
if (a[3] === 97)
if (a.length === 4) {
return 100
}
return 10
}
function ifSingleOptimizedAlt(a) {
if (a.length >= 4 && a[0] == 97 && a[1] == 97 && a[2] == 97 && a[3] == 97 && a.length == 4) {
return 100
}
return 10
}
function ifChainOptimizedAlt(a) {
if (a.length >= 4)
if (a[0] == 97)
if (a[1] == 97)
if (a[2] == 97)
if (a[3] == 97)
if (a.length == 4) {
return 100
}
return 10
}
let paths = [
[97, 97, 97, 97],
[97, 97, 97, 97, 97],
[97, 97, 96, 97],
[97, 96, 97, 97],
[96, 97, 97, 97],
[],
[98],
[97, 97, 97],
[97, 97],
[97],
[97, 97, 96],
[97, 96],
[96],
]
let func1 = [ifSingle, ifChain, ifSingleOptimized, ifChainOptimized, ifSingleOptimizedAlt, ifChainOptimizedAlt];
for (let fun of func1) {
console.log('-- begin', fun.name)
for (var i = 0; i < 1000000; i++) {
paths.map(x => fun(x))
}
printCurrentStatus(fun);
}
printStatusHelperText()

106
benchmark/map_query.mjs Normal file
View file

@ -0,0 +1,106 @@
import { summary, run, bench } from 'mitata';
function printCurrentStatus(fn) {
let opt = %GetOptimizationStatus(fn)
console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`)
}
function printStatusHelperText() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned
lite mode
marked for deoptimization
baseline
topmost frame is interpreted
topmost frame is baseline`)
}
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
function mapGetAndCheck(map, t) {
let x = map.get(t)
if (x) return x
return t
}
function mapCheckBeforeGet(map, t) {
if (map.has(t)) {
return map.get(t)
}
return t
}
let paths = [
'a',
'aa',
'aaa',
'aaaa',
'aaaaa',
'aaaaaa',
'aaaaaaa',
'aaaaaaaa',
]
let tests = [
'a',
'aa',
'aaa',
'aaaa',
'aaaaa',
'aaaaaa',
'aaaaaaa',
'aaaaaaaa',
'b',
'bb',
'bbb',
'bbbb',
'bbbbb',
'bbbbbb',
'bbbbbbb',
'bbbbbbbb',
'c',
'cc',
'ccc',
'cccc',
'ccccc',
'cccccc',
'ccccccc',
'cccccccc',
]
let map = new Map(paths.map(x => [x, { a: x }]))
let func1 = [mapGetAndCheck, mapCheckBeforeGet];
for (let fun of func1) {
console.log('-- begin', fun.name)
for (var i = 0; i < 1000000; i++) {
tests.map(t => fun(map, t))
}
printCurrentStatus(fun);
}
printStatusHelperText()
summary(() => {
func1.forEach(function(fun) {
bench(fun.name, function() {
return paths.map(t => fun(map, t))
})
})
})
run().then(function() {
for (let fun of func1) {
printCurrentStatus(fun);
}
printStatusHelperText()
});

View file

@ -0,0 +1,25 @@
import { getExternalKeys } from '@mapl/compiler';
import { compileRouter as compileRouterContent } from '@mapl/router';
const PATH = "__req_p";
const REQ = '__req';
const PARAMS = `${REQ}_ps`;
export function compileRouter(root) {
const state = {
contentBuilder: [],
declarationBuilders: [],
externalValues: [],
compileItem: (item, state, hasParam) => {
state.contentBuilder.push(`return [f${state.externalValues.push(item)},${hasParam ? PARAMS : '[]'}];`);
}
};
compileRouterContent(root, state);
// eslint-disable-next-line
return Function(
...getExternalKeys(state),
`return (${PATH})=>{${state.contentBuilder.join('')}return null;}`
)(...state.externalValues);
}

View file

@ -0,0 +1,31 @@
import { createRouter, insertItem } from '@mapl/router'
import { compileRouter } from './mapl_compiler.mjs'
import { printTime } from './utils.mjs'
import * as consts from './const.js'
let testPaths = consts.allManyRoutes // consts.allManyRoutes
/*testPaths = [
'/api/works/:id/lock',
'/api/staff/:id',
// '/::rest',
]*/
testPaths = testPaths.map(x => x.replace(/::[^\/]+/g, '**').replace(/:[^\/]+/g, '*'))
const r = createRouter();
for (let route of testPaths) {
let cleaned = route.replace(/:[^\/]+/g, '*')
insertItem(r, cleaned, () => { })
}
let s1 = process.hrtime.bigint()
let s2 = process.hrtime.bigint()
compileRouter(r);
let s3 = process.hrtime.bigint()
let time = s3 - s2 - (s2 - s1)
printTime(time)

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,10 @@
"author": "", "author": "",
"license": "WTFPL", "license": "WTFPL",
"dependencies": { "dependencies": {
"@mapl/router": "^0.0.16",
"benchmarkjs-pretty": "^2.0.0", "benchmarkjs-pretty": "^2.0.0",
"express": "^4.17.1", "express": "^4.17.1",
"koa-router": "^8.0.8" "koa-router": "^8.0.8",
"mitata": "^1.0.10"
} }
} }

221
benchmark/promise.mjs Normal file
View file

@ -0,0 +1,221 @@
import { summary, run, bench } from 'mitata';
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
// Example benchmark
/*summary(() => {
const fn = () => null
const fnAlt = function() {}
function fnBasic() {}
const fnAltWithReturn = function() { return null }
function fnBasicWithReturn() { return null }
bench('Reject with uncached error handler', async () => await Promise.reject().catch(() => null));
bench('Reject with cached error handler', async () => await Promise.reject().catch(fn));
bench('TT Reject with uncached error handler', () => Promise.reject().catch(() => null));
bench('TT Reject with cached error handler', () => Promise.reject().catch(fnAlt));
bench('TT Reject with cached basic error handler', () => Promise.reject().catch(fnBasic));
bench('TT Reject with cached error handler with return', () => Promise.reject().catch(fnAltWithReturn));
bench('TT Reject with cached basic error handler with return ', () => Promise.reject().catch(fnBasicWithReturn));
});*/
/*
summary(() => {
function triple () {
const bla = Math.round(Math.random()) ? true : null
return bla === null
}
function doubleNull () {
const bla = Math.round(Math.random()) ? true : null
return bla == null
}
function doubleUndefined () {
const bla = Math.round(Math.random()) ? true : undefined
return bla == null
}
bench('null === null', triple);
bench('null == null', doubleNull);
bench('undefined == null', doubleUndefined);
bench('2x null === null', triple);
bench('2x null == null', doubleNull);
bench('2x undefined == null', doubleUndefined);
})
summary(() => {
function triple () {
const bla = false ? true : null
return bla === null
}
function doubleNull () {
const bla = false ? true : null
return bla == null
}
function doubleUndefined () {
const bla = false ? true : undefined
return bla == null
}
bench('const null === null', triple);
bench('const null == null', doubleNull);
bench('const undefined == null', doubleUndefined);
bench('const 2x null === null', triple);
bench('const 2x null == null', doubleNull);
bench('const 2x undefined == null', doubleUndefined);
})*/
function printCurrentStatus(fn) {
let opt = %GetOptimizationStatus(fn)
console.log(`${fn.toString()}
${opt.toString(2).padStart(12, '0').split('').join(' ')}`)
}
function printTree() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned`)
}
let func1
let func2
let func3
await (async function () {
return
const fn1 = async () => await Promise.reject().catch(() => null) === null ? 1 : 0;
const fn2 = async () => await Promise.reject().catch(() => {}) == null ? 1 : 0;
const fn3 = async () => await Promise.reject().catch(() => null) == null ? 1 : 0;
const noop = () => null;
const fn4 = async () => await Promise.reject().catch(noop) == null ? 1 : 0;
const fn5 = async () => await Promise.reject().catch(noop) === null ? 1 : 0;
const fn6 = () => Promise.reject().catch(() => null).then(x => x === null ? 1 : 0);
const fn7 = () => Promise.reject().catch(() => {}).then(x => x == null ? 1 : 0);
const fn8 = () => Promise.reject().catch(() => null).then(x => x == null ? 1 : 0);
const fn9 = () => Promise.reject().catch(noop).then(x => x == null ? 1 : 0);
const fn10 = () => Promise.reject().catch(noop).then(x => x === null ? 1 : 0);
func1 = [fn1, fn2, fn3, fn4, fn5, fn6, fn7, fn8, fn9, fn10];
for (let fun of func1) {
%OptimizeFunctionOnNextCall(fun);
}
for (var i = 0; i < 100000; i++) {
for (let fun of func1) {
await fun()
}
}
for (let fun of func1) {
printCurrentStatus(fun);
}
printTree()
summary(() => {
bench('Null with ===', fn1);
bench('Undefined with ==', fn2);
bench('Null with ==', fn3);
bench('Cached Null with ==', fn4);
bench('Cached Null with ===', fn5);
bench('[sync] Null with ===', fn6);
bench('[sync] Undefined with ==', fn7);
bench('[sync] Null with ==', fn8);
bench('[sync] Cached Null with ==', fn9);
bench('[sync] Cached Null with ===', fn10);
});
})
await (async function () {
const alt1 = function () { (Math.round(Math.random()) ? null : 1) ?? 0 };
const alt2 = function () { (Math.round(Math.random()) ? null : 1) || 0 };
const alt3 = function () { (Math.round(Math.random()) ? undefined : 1) ?? 0 };
const alt4 = function () { (Math.round(Math.random()) ? undefined : 1) || 0 };
let check = new Array(100).fill(0).map(() => Math.round(Math.random()) ? null : 1)
let check2 = new Array(100).fill(0).map(() => Math.round(Math.random()) ? undefined : 1)
const alt5 = function () { let out = 0; for (let x of check) { out += x ?? 0 } return out };
const alt6 = function () { let out = 0; for (let x of check) { out += x || 0 } return out };
const alt7 = function () { let out = 0; for (let x of check2) { out += x ?? 0 } return out };
const alt8 = function () { let out = 0; for (let x of check2) { out += x || 0 } return out };
func2 = [alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8];
for (let fun of func2) {
%OptimizeFunctionOnNextCall(fun);
}
for (var i = 0; i < 100000; i++) {
for (let fun of func2) {
fun()
}
}
for (let fun of func2) {
printCurrentStatus(fun);
}
printTree()
summary(() => {
bench('null/1 ?? 0', alt1);
bench('null/1 || 0', alt2);
bench('undefined/1 ?? 0', alt3);
bench('undefined/1 || 0', alt4);
});
summary(() => {
bench('arr null/1 ?? null', alt5);
bench('arr null/1 || null', alt6);
bench('arr und/1 ?? null', alt7);
bench('arr und/1 || null', alt8);
});
})()
await (async function () {
const alt1 = function () { let out = 0; for (let x = 0; x < 100; x++) { out += (Math.round(Math.random()) ? null : 1) ?? 0 } return out };
const alt2 = function () { let out = 0; for (let x = 0; x < 100; x++) { out += (Math.round(Math.random()) ? null : 1) || 0 } return out };
const alt3 = function () { let out = 0; for (let x = 0; x < 100; x++) { out += (Math.round(Math.random()) ? undefined : 1) ?? 0 } return out };
const alt4 = function () { let out = 0; for (let x = 0; x < 100; x++) { out += (Math.round(Math.random()) ? undefined : 1) || 0 } return out };
func3 = [alt1, alt2, alt3, alt4];
for (let fun of func3) {
%OptimizeFunctionOnNextCall(fun);
}
for (var i = 0; i < 100000; i++) {
for (let fun of func3) {
fun()
}
}
for (let fun of func3) {
printCurrentStatus(fun);
}
printTree()
summary(() => {
bench('loop null/1 ?? 0', alt1);
bench('loop null/1 || 0', alt2);
bench('loop und/1 ?? 0', alt3);
bench('loop und/1 || 0', alt4);
});
})()
// Start the benchmark
run().then(() => {
for (let fun of func3) {
printCurrentStatus(fun);
}
printTree()
});

View file

@ -0,0 +1,97 @@
import { summary, run, bench } from 'mitata';
import { createRouter, insertItem } from '@mapl/router'
import { compileRouter } from './mapl_compiler.mjs'
import { compilePaths } from "./router_v2.mjs"
import { compilePaths as mainCompiler } from "../router_v2.mjs"
import { FlaskaRouter as FlaskaRouterBuffer } from "../flaska_buffer.mjs"
import { FlaskaRouter as FlaskaRouterFast } from "../flaska_fast.mjs"
import * as consts from './const.js'
function printCurrentStatus(fn) {
// console.log(`--- checking optimizations status on ${fn.name} ---`)
// let opt = %GetOptimizationStatus(fn)
// console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`)
}
function printStatusHelperText() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned
lite mode
marked for deoptimization
baseline
topmost frame is interpreted
topmost frame is baseline`)
}
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
let paths = consts.allRoutes.map(x => ({ path: x }))
let tests = paths.map(p => ([p.path.replace(/:[^/]+/g, '_'), p]))
let testStrings = tests.map(x => x[0])
let testStringsMapl = testStrings.map(x => x.slice(1))
let func = [[testStrings, mainCompiler(paths)]]
let flaskaRouterBuffer = new FlaskaRouterBuffer()
flaskaRouterBuffer.paths = paths.slice()
flaskaRouterBuffer.compile()
func.push([testStrings, flaskaRouterBuffer.match])
let flaskaRouterFast = new FlaskaRouterFast()
flaskaRouterFast.paths = paths.slice()
flaskaRouterFast.compile()
func.push([testStrings, flaskaRouterFast.match])
let maplPaths = paths.map(x => x.path.replace(/::[^\/]+/g, '**').replace(/:[^\/]+/g, '*'))
const maplRouter = createRouter();
for (let route of maplPaths) {
insertItem(maplRouter, route, { path: route })
}
let maplMatcher = compileRouter(maplRouter)
func.push([testStringsMapl, maplMatcher])
for (let [tests, fun] of func) {
console.log(`--- warming up ${fun.name || 'mapl'} ---`)
for (var i = 0; i < 100000; i++) {
tests.forEach(fun)
}
}
for (let [tests, fun] of func) {
console.log(`--- Sanity checking ${fun.name || 'mapl'} ---`)
for (let a of tests) {
// console.log(a, fun(a))
}
}
console.log('--- sleeping ---')
await new Promise(res => setTimeout(res, 1000))
for (let [_, org] of func) {
printCurrentStatus(org);
}
printStatusHelperText()
summary(() => {
func.forEach(function([tests, fun]) {
// console.log(tests, fun, tests.map(fun))
bench(fun.name || 'mapl', function() {
return tests.map(fun)
})
})
})
run().then(function() {
for (let [_, check] of func) {
printCurrentStatus(check);
}
printStatusHelperText()
});

517
benchmark/router_v2.mjs Normal file
View file

@ -0,0 +1,517 @@
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)],
]
}

View file

@ -0,0 +1,16 @@
import { compilePaths } from "../router_v2.mjs"
import { printTime } from './utils.mjs'
import * as consts from './const.js'
let paths = consts.allManyRoutes.map(x => ({ path: x }))
let s1 = process.hrtime.bigint()
let s2 = process.hrtime.bigint()
compilePaths(paths)
let s3 = process.hrtime.bigint()
let time = s3 - s2 - (s2 - s1)
printTime(time)

117
benchmark/set_arr.mjs Normal file
View file

@ -0,0 +1,117 @@
import { summary, run, bench } from 'mitata';
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
function padStart(length) {
return ''.padStart(length * 2)
}
const data = [
'/',
'/api/articles',
'/api/articles/:id/file',
'/api/articles/:id',
'/api/articles/public',
'/api/articles/public/:id',
'/api/categories',
'/api/categories/:categoryId/products',
'/api/categories/:categoryId/properties',
'/api/categories/:categoryId/values/:props',
'/api/categories/:categoryId',
//'/api/categories/:categoryId/products/:productId',
'/api/categories/:categoryId/products/:productId',
'/api/customers',
'/api/customers/:id',
'/api/customers/kennitala/:kennitala',
'/api/customers/public/kennitala/:kennitala',
'/api/customers/search/:search',
'/api/file',
'/api/file/:id',
'/api/media',
'/api/media/:id',
'/api/orderitem',
'/api/orderitem/:id',
'/api/orders',
'/api/orders/:orderId',
'/api/orders/:orderId/sell',
'/api/pages',
'/api/pages/:pageId',
'/api/pages/:pageId/articles',
'/api/pages/:pageId/articles/public',
'/api/products',
'/api/products/:id',
'/api/products/:id/movement',
'/api/products/:id/sub_products/:productId',
//'/api/products/:id/sub_products/:productId',
'/api/products/code/:code',
'/api/products/property/:propertyId',
'/api/properties',
'/api/properties/:id',
'/api/sales',
'/api/sales/:id',
'/api/stockitem',
'/api/stockitem/:id',
'/api/stocks',
'/api/stocks/:id',
'/api/stocks/:id/commit',
'/api/test',
'/api/test/auth',
'/api/test/error',
'/api/workentries',
'/api/workentries/:id',
'/api/works',
'/api/works/:id',
'/api/works/:id/lock',
'/api/works/public',
'/api/staff',
'/api/staff/:id',
'/::rest',
]
function arrIncludes(data) {
let out = new Array()
for (let item of data) {
if (out.includes(item)) { break }
out.push(item)
}
return out
}
function setAdd(data) {
let s = new Set()
for (let item of data) {
let size = s.size
if (s.add(item).size === size) { break }
}
return s
}
let func = [arrIncludes, setAdd];
for (let fun of func) {
console.log(`--- warming up ${fun.name || 'mapl'} ---`)
for (var i = 0; i < 100; i++) {
fun(data)
}
}
await new Promise(res => setTimeout(res, 3000))
summary(() => {
for (let i = 20; i >= 0; i--) {
const dataSet = data.slice(0, data.length - i)
func.forEach(function(fun) {
bench(`${dataSet.length} items: ${fun.name}`, function() {
return fun(dataSet)
})
})
}
// console.log(tests, fun, tests.map(fun))
/*bench(fun.name, function() {
return fun(data)
})*/
})
run();

80
benchmark/string.js Normal file
View file

@ -0,0 +1,80 @@
import { summary, run, bench } from 'mitata';
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
// Example benchmark
summary(() => {
const dataset = new Array(100).fill(0).map(
() => new Array(10).fill(0).map(() => String.fromCharCode(97 + Math.round(Math.random() * 22))).join('')
);
console.log(dataset)
const fn1 = (str) => (str[0] === 'c' ? 1 : 0) + (str[1] === 'c' ? 1 : 0) + (str[2] === 'c' ? 1 : 0);
fn1('a');
fn1('b');
fn1('c');
fn1('d');
// optimizeNextInvocation(fn1);
bench('Char check', () => dataset.map(fn1))
const fn2 = (str) => (str.charCodeAt(0) === 99 ? 1 : 0) + (str.charCodeAt(1) === 99 ? 1 : 0) + (str.charCodeAt(2) === 99 ? 1 : 0);
fn2('a');
fn2('b');
fn2('c');
fn2('d');
// optimizeNextInvocation(fn2);
bench('Char code check', () => dataset.map(fn2));
bench('2x Char check', () => dataset.map(fn1))
bench('2x Char code check', () => dataset.map(fn2));
});
// Example benchmark
summary(() => {
let paths = [
'test1/',
'test/',
'test3/',
'test/',
'test5/',
'something/',
'else/',
'goes/',
'here/',
'too/',
]
function fastCheck(str) {
if (str.charCodeAt(0) === 116) {
if (str.charCodeAt(1) === 101) {
if (str.charCodeAt(2) === 115) {
if (str.charCodeAt(3) === 116) {
if (str.charCodeAt(4) === 47) {
if (str.indexOf('/', 5) === -1) {
return true
}
}
}
}
}
}
return false
}
let r = new RegExp('^test/$')
function regexCheck(str) {
return r.test(str)
}
console.log(paths.map(fastCheck))
console.log(paths.map(regexCheck))
bench('fastCheck', () => paths.map(fastCheck))
bench('regexCheck', () => paths.map(regexCheck));
});
run();

121
benchmark/strings.mjs Normal file
View file

@ -0,0 +1,121 @@
import { buildTree, compileTreeIntoIfs, compileTreeIntoIfsWithBuffer } from "./compiler/compiler.mjs"
import { summary, run, bench } from 'mitata';
import { printCurrentStatus, printStatusHelperText } from "./compiler/utils.mjs";
import { FlaskaRouter } from "../flaska.mjs";
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
let paths = [
'/fsdafasfa',
'/ymreklhmse',
'/h34nmlaeknmgl',
'/asdgsdagas',
'/ahaewhweaaa',
'/adshashaea',
'/sdafasfsadfasdfas',
//'/gdfsfgsfdsgsdrgsregsergsregersgserersgsergersg',
//'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
]
let pathsBla = [
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
]
let pathsBuffer = paths.map(x => new Uint8Array(Buffer.from(x)))
let tree = buildTree(paths)
let noop = function() { }
const ifTree = compileTreeIntoIfs(tree)
// console.log(ifTree.toString())
const ifTreeBuffer = compileTreeIntoIfsWithBuffer(tree)
// console.log(ifTreeBuffer.toString())
const flaskaRouter = new FlaskaRouter()
for (let path of paths) {
flaskaRouter.addRoute(path, noop)
}
const m = new Map(paths.map(x => [x, x]))
function mapFlat(str) {
return m.get(str) ?? null
}
function toBuffer(str) {
return Buffer.from(str)
}
function toUint(str) {
return new Uint8Array(Buffer.from(str))
}
function toManualArray(str) {
let length = str.length
let out = new Array(length)
for (let i = 0; i < length; i++) {
out[i] = str.charCodeAt(i)
}
return out
}
function allocBufferUnsafe(str) {
return Buffer.allocUnsafe(str.length)
}
function allocBufferSafe(str) {
return Buffer.alloc(str.length)
}
function allocUint8(str) {
return new Uint8Array(str.length)
}
function toManualArraySplitMap(str) {
return str.split('').map(x => x.charCodeAt(0))
}
let func1 = [mapFlat, toBuffer, toUint, toManualArray, toManualArraySplitMap, allocBufferUnsafe, allocBufferSafe, allocUint8, ifTree];
for (var i = 0; i < 100000; i++) {
for (let fun of func1) {
paths.map(fun)
}
}
for (var i = 0; i < 100000; i++) {
for (let path of paths) {
flaskaRouter.match(path)
}
}
for (let fun of func1) {
printCurrentStatus(fun);
}
printCurrentStatus(flaskaRouter.match)
printStatusHelperText()
summary(() => {
bench('if tree', function() {
return paths.map(ifTree)
})
bench('if tree buffer edition', function() {
return paths.map(ifTreeBuffer)
});
bench('flaskarouter', function() {
return paths.map(flaskaRouter.match.bind(flaskaRouter))
});
/*bench('map edition', function() {
return paths.map(mapFlat)
});
bench('if tree pre buffer edition', function() {
return pathsBuffer.map(ifTreeBuffer)
});
bench('toBuffer', () => pathsBla.map(toBuffer));
bench('toUint', () => pathsBla.map(toUint));
bench('toManualArraySplitMap', () => pathsBla.map(toManualArraySplitMap))
bench('toManualArray', () => pathsBla.map(toManualArray))*/
bench('allocBufferUnsafe', () => pathsBla.map(allocBufferUnsafe))
bench('allocBufferSafe', () => pathsBla.map(allocBufferSafe))
bench('allocUint8', () => pathsBla.map(allocUint8))
})
run();

View file

@ -0,0 +1,115 @@
import { printCurrentStatus, printStatusHelperText } from "./compiler/utils.mjs";
import { summary, run, bench } from 'mitata';
// Do warmup
bench('noop', () => { })
bench('noop2', () => { })
// Do checks with arrays
let arr = ['hi', 'there', 'nagna', 'randomlongstring', 'small']
arr = arr.map(x => [x, { path: x }])
let paths = arr.map(x => x[1])
// Do checks with objects
let obj = {};
let map = new Map()
for (let i = 0, l = arr.length; i < l; ++i) {
obj[arr[i][0]] = arr[i][1]
map.set(arr[i][0], arr[i][1])
};
let objHas1 = (obj, item) => { let x = obj[item]; if (x) { return x } return null };
let objHas2 = (obj, item) => { if (item in obj) { return obj[item] } return null }
let mapGet = (map, item) => { let x = map.get(item); if (x) { return x }; return null };
// Generate a function which compares the input string to all strings in the list
let basicIfChain = Function(`
return (paths, str) => {
${arr.map((item, i) => 'if (str === "' + item[0] + '") { return paths[' + i + '] }').join('\n ')}
return null
}
`)();
let basicIfChainAlt = Function(`
return (str) => {
${arr.map((item, i) => 'if (str === "' + item[0] + '") { return "' + item[0] + '" }').join('\n ')}
return null
}
`)();
function basicIfChainNoFunc(str) {
if (str === "hi") { return "hi" }
if (str === "there") { return "there" }
if (str === "nagna") { return "nagna" }
if (str === "randomlongstring") { return "randomlongstring" }
if (str === "small") { return "small" }
return null
}
function addTwo(a, b) {
return a + b
}
console.log(`
return (str) => {
${arr.map((item, i) => 'if (str === "' + item[0] + '") { return "' + item[0] + '" }').join('\n ')}
return null
}
`)
let func = [
['basicIfChainNoFunc', basicIfChainNoFunc, basicIfChainNoFunc],
['basicIfChainAlt', basicIfChainAlt, basicIfChainAlt],
['objHas1', objHas1, objHas1.bind(null, obj)],
['objHas2', objHas2, objHas2.bind(null, obj)],
['mapGet', mapGet, mapGet.bind(null, map)],
['basicIfChain', basicIfChain, basicIfChain.bind(null, paths)],
]
for (let [name, __, fun] of func) {
console.log(`--- Sanity checking ${name} ---`)
for (let a of arr) {
console.log(a[0], fun(a[0]))
}
}
// Generate strings
let newArr = [];
for (let i = 0; i < 100000; i++)
newArr.push(Math.random() < 0.5 ? `${Math.random()}` : arr[Math.round(Math.random() * 4)].slice(0, -1 >>> 0));
console.log(`--- warming up addTwo ---`)
for (let i = 0; i < 100; i++) {
console.log(`--- run ${i} ---`)
for (let num of newArr) {
addTwo(num.length, num.length + 1)
}
await new Promise(res => setTimeout(res, 1000))
printCurrentStatus(addTwo);
}
for (let [name, org, fun] of func) {
console.log(`--- warming up ${name} ---`)
for (let i = 0; i < 100; i++) {
console.log(`--- run ${i} ---`)
newArr.forEach(fun)
await new Promise(res => setTimeout(res, 1000))
printCurrentStatus(org);
}
}
console.log('--- sleeping ---')
await new Promise(res => setTimeout(res, 1000))
for (let [_, org] of func) {
printCurrentStatus(org);
}
printStatusHelperText()
summary(() => {
func.forEach(([name, _, fun]) => {
bench(name, () => newArr.map(fun));
})
});
run();

44
benchmark/strings_v2.mjs Normal file
View file

@ -0,0 +1,44 @@
import { summary, run, bench } from 'mitata';
import { FlaskaRouter } from "../flaska.mjs";
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
function padStart(length) {
return ''.padStart(length * 2)
}
const spaces = ''.padStart(256)
function strSlice(length) {
return spaces.slice(0, length)
}
function strSubstring(length) {
return spaces.substring(0, length)
}
const testData = new Array(100000)
for (let i = 0; i < testData.length; i++) {
testData[i] = Math.round(Math.random() * 200)
}
let func = [padStart, strSlice, strSubstring];
for (let fun of func) {
console.log(`--- warming up ${fun.name || 'mapl'} ---`)
for (var i = 0; i < 100; i++) {
testData.map(fun)
}
}
summary(() => {
func.forEach(function(fun) {
// console.log(tests, fun, tests.map(fun))
bench(fun.name, function() {
return testData.map(fun)
})
})
})
run();

33
benchmark/test.mjs Normal file
View file

@ -0,0 +1,33 @@
import assert from 'assert'
import { compilePaths } from "./router_v2.mjs"
import * as consts from './const.js'
let paths = [
{ path: '/aa/aa', },
{ path: '/aa/:blabla', },
{ path: '/::rest', },
]
// paths = consts.allManyRoutes.map(x => ({ path: x }))
let tests = [
['/', paths[5]],
['/aa', paths[5]],
['/aa/aa', paths[0]],
['/aa/_', paths[1]],
['/aa/_/aa', paths[2]],
['/aa/_/ab', paths[3]],
['/aa/_/bb', paths[4]],
]
tests = paths.map(p => ([p.path.replace(/:[^/]+/g, '_'), p]))
let func = compilePaths(paths)
for (let [_, fun] of func) {
console.log(`--- ${fun.name} ---`)
for (let test of tests) {
let check = fun(test[0])
console.log(test[0], check)
assert.strictEqual(check.path, test[1])
}
}

15
benchmark/utils.mjs Normal file
View file

@ -0,0 +1,15 @@
export function printTime (t) {
let time = Number(t)
let units = ['n', 'μ', 'm', 'c', 's']
let unit = units[0]
let unitPower = 1
for (let i = 0; i < units.length; i++) {
let power = Math.pow(10, (i + 1) * 3)
if (power * 2 > time) {
break
}
unitPower = power
unit = units[i]
}
console.log(t, '=', Number((time / unitPower).toFixed(2)), unit)
}

View file

@ -37,16 +37,6 @@ export const MimeTypeDb = getDb()
* Router * Router
*/ */
class Branch {
constructor() {
this.children = new Map()
this.paramName = null
this.fullparamName = null
this.handler = null
this.middlewares = []
}
}
export const ErrorCodes = { export const ErrorCodes = {
ERR_CONNECTION_ABORTED: 'ERR_CON_ABORTED' ERR_CONNECTION_ABORTED: 'ERR_CON_ABORTED'
} }
@ -73,8 +63,6 @@ const statuses = {
304: true 304: true
} }
} }
const __paramMapName = '__param'
const __fullParamMapName = '__fullparam'
function assertIsHandler(handler, name) { function assertIsHandler(handler, name) {
if (typeof(handler) !== 'function') { if (typeof(handler) !== 'function') {
@ -114,9 +102,9 @@ export function JsonHandler(org = {}) {
ctx.req.body = {} ctx.req.body = {}
return return
} }
const data = Buffer.concat(buffers).toString(); const data = Buffer.concat(buffers).toString();
try { try {
ctx.req.body = JSON.parse(data) ctx.req.body = JSON.parse(data)
} catch (err) { } catch (err) {
@ -145,7 +133,7 @@ export function CorsHandler(opts = {}) {
// Always add vary header on origin. Prevent caches from // Always add vary header on origin. Prevent caches from
// accidentally caching wrong preflight request // accidentally caching wrong preflight request
ctx.headers['Vary'] = 'Origin' ctx.headers['Vary'] = 'Origin'
// Set status to 204 if OPTIONS. Just handy for flaska and // Set status to 204 if OPTIONS. Just handy for flaska and
// other checking. // other checking.
if (ctx.method === 'OPTIONS') { if (ctx.method === 'OPTIONS') {
@ -228,7 +216,7 @@ export function FormidableHandler(formidable, org = {}) {
// For testing/stubbing purposes // For testing/stubbing purposes
let rename = formidable.fsRename || fs.rename let rename = formidable.fsRename || fs.rename
return function(ctx) { return function(ctx) {
let form = formidable.IncomingForm() let form = formidable.IncomingForm()
form.uploadDir = opts.uploadDir form.uploadDir = opts.uploadDir
@ -263,7 +251,7 @@ export function FormidableHandler(formidable, org = {}) {
keys.map(key => { keys.map(key => {
let filename let filename
let target let target
try { try {
filename = opts.filename(ctx.req.files[key]) || ctx.req.files[key].name filename = opts.filename(ctx.req.files[key]) || ctx.req.files[key].name
target = path.join(opts.uploadDir, filename) target = path.join(opts.uploadDir, filename)
@ -386,7 +374,7 @@ export class FileResponse {
} }
} }
} }
let ext = path.extname(this.filepath).slice(1) let ext = path.extname(this.filepath).slice(1)
let found = MimeTypeDb[ext] let found = MimeTypeDb[ext]
if (found) { if (found) {
@ -403,190 +391,322 @@ export class FileResponse {
} }
} }
/*
* --- Router ---
*/
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 = []
}
const regParamPrefix = /^::?/
const regCleanNonAschii = /(?![a-zA-Z_])./g
const regCleanRest = /_+/g
const regStarDoubleParam = /::[^:/]+/g
const regStarSingleParam = /:[^:/]+/g
const spaces = ' '
export class FlaskaRouter { export class FlaskaRouter {
constructor() { constructor() {
this.root = new Branch() this.paths = []
this.registeredPaths = new Set()
} }
addRoute(route, orgMiddlewares, orgHandler) { addRoute(path, middlewares, orgHandler) {
if (route[0] !== '/') if (path[0] !== '/')
throw new Error(`route "${route}" must start with forward slash`) throw new RouterError(null, null, `addRoute("${path}") path must start with forward slash`)
let middlewares = orgMiddlewares let cleaned = path
let handler = orgHandler if (cleaned.indexOf('/:') >= 0) {
if (!orgHandler) { cleaned = cleaned.replace(regStarDoubleParam, '**').replace(regStarSingleParam, '*')
handler = orgMiddlewares if (cleaned.indexOf(':') > 0) {
middlewares = [] throw new RouterError(null, null, `addRoute("${path}") path has missing name or word between two forward slashes`)
}
if (middlewares && typeof(middlewares) === 'function') {
middlewares = [middlewares]
}
assertIsHandler(handler, 'addRoute()')
let start = 1
let end = 1
let name = ''
let param = ''
let isParam = false
let isFullParam = false
let branch = this.root
if (route.indexOf(':') < 0) {
let name = route
if (name.length > 1 && name[name.length - 1] === '/') {
name = name.slice(0, -1)
} }
let child = new Branch() if (cleaned.indexOf('**/') > 0) {
branch.children.set(name, child) throw new RouterError(null, null, `addRoute("${path}") cannot add anything after a full param route`)
child.handler = handler
child.middlewares = middlewares
}
for (let i = 1; i <= route.length; i++) {
if ((i === route.length || route[i] === '/') && end > start) {
if (branch.fullparamName) {
throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`)
}
let child
name = route.substring(start, end)
if (isFullParam) {
param = name
name = __fullParamMapName
} else if (isParam) {
param = name
name = __paramMapName
}
if (branch.children.has(name)) {
child = branch.children.get(name)
}
else if (isParam && !isFullParam && branch.children.has(__fullParamMapName)) {
throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`)
}
else if (isFullParam && branch.children.has(__paramMapName)) {
throw new Error(`route "${route}" conflicts with a sub-branch that has a partial param child`)
}
else {
child = new Branch()
branch.children.set(name, child)
}
branch = child
end = i
start = i
if (isParam) {
if (branch.paramName && branch.paramName !== param) {
throw new Error(`route "${route}" conflicts with pre-existing param name of ${branch.paramName} instead of ${param}`)
}
if (isFullParam) {
branch.fullparamName = param
} else {
branch.paramName = param
}
isParam = false
}
} else if (route[i] === '/' && end === start) {
throw new Error(`route "${route}" has missing path name inbetween slashes`)
}
if (i === route.length) {
branch.handler = handler
branch.middlewares = middlewares
continue
}
if (route[i] === ':') {
if (isParam) {
isFullParam = true
}
isParam = true
end = start = i + 1
}
else if (route[i] === '/') {
end = start = i + 1
}
else {
end++
} }
} }
if (cleaned.indexOf('//') >= 0) {
throw new RouterError(null, null, `addRoute("${path}") path has missing name or word between two forward slashes`)
}
let size = this.registeredPaths.size
if (this.registeredPaths.add(cleaned).size === size) {
throw new RouterError(null, null, `addRoute("${path}") found an existing route with same path of ${cleaned}`)
}
let handlers = []
if (Array.isArray(middlewares)) {
handlers.push(...middlewares)
if (typeof(orgHandler) !== 'function') {
throw new RouterError(orgHandler, null, `addRoute("${path}") was called with a handler that was not a function`)
}
} else {
handlers.push(middlewares)
}
if (orgHandler) {
handlers.push(orgHandler)
}
for (let handler of handlers) {
if (typeof(handler) !== 'function') {
throw new RouterError(handler, null, `addRoute("${path}") was called with a handler that was not a function`)
}
}
this.paths.push({
path,
handlers
})
} }
match(orgUrl) { __buildChild(x, i, splitPaths) {
let url = orgUrl let splitPath = splitPaths[0]
if (url.length > 1 && url[url.length - 1] === '/') { let letter = new Child(splitPath.split, x, i)
url = url.slice(0, -1)
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]
} }
let branch = this.root
let start = 1 for (let y = 1; y < splitPaths.length; y++) {
let end = 1 let checkPath = splitPaths[y]
let output if (!checkPath.split[x]
let name || checkPath.split[x].isParams !== splitPath.split[x].isParams
let char || checkPath.split[x].isFullParams !== splitPath.split[x].isFullParams
let params = {} || !checkPath.split[x].isParams
if (output = branch.children.get(url)) { && !checkPath.split[x].isFullParams
return { && (checkPath.split[x].word[i] || '/') !== letter.char) break
handler: output.handler, consume.push(checkPath)
middlewares: output.middlewares, }
params: params,
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(this.__buildChild(x, i + 1, consume))
consume.splice(0, letter.children[letter.children.length - 1].count)
}
return letter
}
__buildTree(splitPaths) {
let builder = []
while (splitPaths.length) {
builder.push(this.__buildChild(0, 0, splitPaths))
splitPaths.splice(0, builder[builder.length - 1].count)
}
return builder
}
__splitAndSortPaths(paths, separateStatic = true) {
let staticPaths = new Map()
let paramsPaths = []
let collator = new Intl.Collator('en', { sensitivity: 'accent' });
let sealed = Object.seal({})
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: sealed,
})
} }
}
for (let i = 1; i <= url.length; i++) { // Collect params path separately
char = url[i] paramsPaths.push({
if ((i === url.length || char === '/') && end > start) { split: entry.path.slice(1).split(/\//g).map(function(word) {
name = url.slice(start, end) let actualWord = word.replace(regParamPrefix, '')
if (output = branch.children.get(name)) {
branch = output
}
else if (output = branch.children.get(__paramMapName)) {
branch = output
params[branch.paramName] = name
}
else if (output = branch.children.get(__fullParamMapName)) {
params[output.fullparamName] = url.slice(start)
return { return {
handler: output.handler, word: actualWord,
middlewares: output.middlewares, isParams: word[0] === ':' && word[1] !== ':',
params: params, 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,
}
}
__getIndex(offset, additions, params) {
return (offset + additions)
+ (params.length
? ' + ' + params.map(a => `offset${a[1]}`).join(' + ')
: '')
}
__treeIntoCompiledCodeReturnPath(indentString, paths, branch, params, pathParamMapIndex) {
let pathIndex = paths.indexOf(branch.path)
if (pathIndex < 0) {
throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths')
}
let mapIndex = pathParamMapIndex.size + 1
pathParamMapIndex.set(mapIndex, pathIndex)
let output = '\n' + indentString + `return {`
output += '\n' + indentString + ` path: paths_${mapIndex},`
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 + ` {},`
}
output += '\n' + indentString + `}`
return output
}
__treeIntoCompiledCodeBranch(paths, branches, indent = 0, params = [], pathParamMapIndex) {
let output = ''
let indentation = spaces.slice(0, (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 { } else {
if (output = this.root.children.get(__fullParamMapName)) { // output += '} //'
params = { output += '\n' + indentation
[output.fullparamName]: url.slice(1)
}
return {
handler: output.handler,
middlewares: output.middlewares,
params: params,
}
}
return null
}
i++
end = start = i
char = url[i]
}
// Check branch.handler. This can happen if route /::path is added
// and request is '/' it will attempt to match root which will fail
if (i >= url.length && branch.handler) {
return {
handler: branch.handler,
middlewares: branch.middlewares,
params: params,
} }
} }
if (char === '/') {
end = start = i + 1 if (!branch.isParams && !branch.isFullParams) {
output += `if (str.charCodeAt(${this.__getIndex(indent, 0, params)}) === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (strLength === ${this.__getIndex(indent, 1, params)}) {`
output += this.__treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params, pathParamMapIndex)
output += '\n' + indentation + ` }`
}
} else { } else {
end++ addEndBracket = false
let paramVarName = (params.length + 1) + '_' + branch.paramVarName
output += `let s${paramVarName} = str.slice(${this.__getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, str.indexOf('/', ${this.__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 += this.__treeIntoCompiledCodeReturnPath(indentation, paths, branch, params, pathParamMapIndex)
} else if (branch.path) {
output += '\n' + indentation + `if (strLength === ${this.__getIndex(indent, 0, params)}) {`
output += this.__treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params, pathParamMapIndex)
output += '\n' + indentation + `}`
}
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += this.__treeIntoCompiledCodeBranch(paths, branch.children, indent + 1, params.slice(), pathParamMapIndex)
}
if (addEndBracket) {
output += '\n' + indentation + '} '
} }
} }
if (output = this.root.children.get(__fullParamMapName)) { return output
params = { }
[output.fullparamName]: url.slice(1)
} __treeIntoCompiledCodeClosure(paths, tree, staticList) {
return { let pathParamMapIndex = new Map()
handler: output.handler, let output = ''
middlewares: output.middlewares, let prefix = ''
params: params, if (staticList.size > 0) {
} output += '\n let checkStatic = staticList.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
} }
return null if (tree.length) {
output += '\n let strLength = str.length'
output += '\n ' + this.__treeIntoCompiledCodeBranch(paths, tree, 1, [], pathParamMapIndex)
}
output += '\n return null'
output += '\n}'
pathParamMapIndex.forEach(function (val, key) {
prefix += `let paths_${key} = paths[${val}]\n`
})
output = prefix + 'return function flaskaFastRouter(str) {\n "use strict";' + output
return new Function('paths', 'staticList', output)(paths, staticList)
}
compile() {
let splitPaths = this.__splitAndSortPaths(this.paths)
let tree = this.__buildTree(splitPaths.paramsPaths.slice())
this.match = this.__treeIntoCompiledCodeClosure(this.paths, tree, splitPaths.staticPaths)
}
match(url) {
this.compile()
return this.match(url)
} }
} }
@ -682,7 +802,7 @@ export class Flaska {
for (let i = 0; i < this._nonces.length; i++) { for (let i = 0; i < this._nonces.length; i++) {
this._nonces[i] = crypto.randomBytes(16).toString('base64') this._nonces[i] = crypto.randomBytes(16).toString('base64')
} }
constructFunction += ` constructFunction += `
let nonce = this._nonces[this._noncesIndex] || crypto.randomBytes(16).toString('base64'); let nonce = this._nonces[this._noncesIndex] || crypto.randomBytes(16).toString('base64');
this._noncesIndex--; this._noncesIndex--;
@ -692,7 +812,7 @@ ctx.state.nonce = nonce;
constructFunction += 'ctx.headers = {' constructFunction += 'ctx.headers = {'
constructFunction += `'Date': new Date().toUTCString(),` constructFunction += `'Date': new Date().toUTCString(),`
for (let key of headerKeys) { for (let key of headerKeys) {
if (key === 'Content-Security-Policy' && options.nonce.length) { if (key === 'Content-Security-Policy' && options.nonce.length) {
let groups = options.defaultHeaders[key].split(';') let groups = options.defaultHeaders[key].split(';')
for (let ni = 0; ni < options.nonce.length; ni++) { for (let ni = 0; ni < options.nonce.length; ni++) {
@ -754,12 +874,6 @@ ctx.state.nonce = nonce;
this.patch = this.routers.PATCH.addRoute.bind(this.routers.PATCH) this.patch = this.routers.PATCH.addRoute.bind(this.routers.PATCH)
} }
_assertIsHandler(handler, name) {
if (typeof(handler) !== 'function') {
throw new Error(`${name} was called with a handler that was not a function`)
}
}
devMode() { devMode() {
this._backuperror = this._onerror = function(err, ctx) { this._backuperror = this._onerror = function(err, ctx) {
ctx.log.error(err) ctx.log.error(err)
@ -795,7 +909,7 @@ ctx.state.nonce = nonce;
assertIsHandler(handler, 'onreqerror()') assertIsHandler(handler, 'onreqerror()')
this._onreqerror = handler this._onreqerror = handler
} }
onreserror(handler) { onreserror(handler) {
assertIsHandler(handler, 'onreserror()') assertIsHandler(handler, 'onreserror()')
this._onreserror = handler this._onreserror = handler
@ -810,7 +924,7 @@ ctx.state.nonce = nonce;
assertIsHandler(handler, 'beforeAsync()') assertIsHandler(handler, 'beforeAsync()')
this._beforeAsync.push(handler) this._beforeAsync.push(handler)
} }
after(handler) { after(handler) {
assertIsHandler(handler, 'after()') assertIsHandler(handler, 'after()')
this._after.push(handler) this._after.push(handler)
@ -820,7 +934,7 @@ ctx.state.nonce = nonce;
assertIsHandler(handler, 'afterAsync()') assertIsHandler(handler, 'afterAsync()')
this._afterAsync.push(handler) this._afterAsync.push(handler)
} }
requestStart(req, res) { requestStart(req, res) {
let url = req.url let url = req.url
let search = '' let search = ''
@ -894,37 +1008,25 @@ ctx.state.nonce = nonce;
ctx.params = route.params ctx.params = route.params
if (route.middlewares.length) { let handlers = this.runHandlers(ctx, route.path.handlers, 0)
let middle = this.handleMiddleware(ctx, route.middlewares, 0)
if (middle && middle.then) { if (handlers && handlers.then) {
return middle.then(() => { return handlers.then(() => {
return route.handler(ctx)
})
.then(() => {
this.requestEnd(null, ctx)
}, err => {
this.requestEnd(err, ctx)
})
}
}
let handler = route.handler(ctx)
if (handler && handler.then) {
return handler.then(() => {
this.requestEnd(null, ctx) this.requestEnd(null, ctx)
}, err => { }, err => {
this.requestEnd(err, ctx) this.requestEnd(err, ctx)
}) })
} }
this.requestEnd(null, ctx) this.requestEnd(null, ctx)
} }
handleMiddleware(ctx, middles, index) { runHandlers(ctx, middles, index) {
for (let i = index; i < middles.length; i++) { for (let i = index; i < middles.length; i++) {
let res = middles[i](ctx) let res = middles[i](ctx)
if (res && res.then) { if (res && res.then) {
return res.then(() => { return res.then(() => {
return this.handleMiddleware(ctx, middles, i + 1) return this.runHandlers(ctx, middles, i + 1)
}) })
} }
} }
@ -952,7 +1054,7 @@ ctx.state.nonce = nonce;
return return
} }
if (ctx.body === null && !handleUsed && ctx.status === 200) { if (ctx.body == null && !handleUsed && ctx.status === 200) {
ctx.status = 204 ctx.status = 204
} }
@ -989,7 +1091,7 @@ ctx.state.nonce = nonce;
} }
let length = 0 let length = 0
if (body instanceof Buffer) { if (body instanceof Buffer) {
length = body.byteLength length = body.byteLength
ctx.type = ctx.type || 'application/octet-stream' ctx.type = ctx.type || 'application/octet-stream'
@ -1060,6 +1162,12 @@ ctx.state.nonce = nonce;
this[`_${type}AsyncCompiled`] = func.bind(this, ...this[`_${type}Async`]) this[`_${type}AsyncCompiled`] = func.bind(this, ...this[`_${type}Async`])
} }
} }
this.routers.GET.compile()
this.routers.POST.compile()
this.routers.PUT.compile()
this.routers.DELETE.compile()
this.routers.OPTIONS.compile()
this.routers.PATCH.compile()
} }
create() { create() {
@ -1069,7 +1177,7 @@ ctx.state.nonce = nonce;
this.server.on('connection', function (socket) { this.server.on('connection', function (socket) {
// Set socket idle timeout in milliseconds // Set socket idle timeout in milliseconds
socket.setTimeout(1000 * 60 * 5) // 5 minutes socket.setTimeout(1000 * 60 * 5) // 5 minutes
// Wait for timeout event (socket will emit it when idle timeout elapses) // Wait for timeout event (socket will emit it when idle timeout elapses)
socket.on('timeout', function () { socket.on('timeout', function () {
// Call destroy again // Call destroy again
@ -1090,7 +1198,7 @@ ctx.state.nonce = nonce;
} }
this.create() this.create()
this.server.listen(port, ip, cb) this.server.listen(port, ip, cb)
} }
@ -1121,8 +1229,9 @@ ctx.state.nonce = nonce;
if (err) { return rej(err) } if (err) { return rej(err) }
// Waiting 0.1 second for it to close down // Waiting 0.1 second for it to close down
setTimeout(function() { res() }, 100) setTimeout(res, 100)
}) })
this.server.closeAllConnections()
}) })
} }
} }

View file

@ -1,21 +1,18 @@
{ {
"name": "flaska", "name": "flaska",
"version": "1.3.5", "version": "1.4.0",
"description": "Flaska is a micro web-framework for node. It is designed to be fast, simple and lightweight, and is distributed as a single file module with no dependencies.", "description": "Flaska is a micro web-framework for node. It is designed to be fast, simple and lightweight, and is distributed as a single file module with no dependencies.",
"main": "flaska.mjs", "main": "flaska.mjs",
"scripts": { "scripts": {
"test": "eltro", "test": "eltro -r dot",
"test:watch": "npm-watch test" "test:watch": "eltro -r dot -w test"
}, },
"watch": { "watch": {
"test": { "test": {
"patterns": [ "patterns": [
"test/*", "./"
"flaska.mjs"
], ],
"extensions": "mjs", "extensions": "mjs"
"quiet": true,
"inherit": true
} }
}, },
"type": "module", "type": "module",
@ -42,6 +39,11 @@
"eltro": "^1.3.2", "eltro": "^1.3.2",
"formidable": "^1.2.2" "formidable": "^1.2.2"
}, },
"pnpm": {
"allowedDeprecatedVersions": {
"formidable": "1"
}
},
"files": [ "files": [
"flaska.mjs", "flaska.mjs",
"README.md", "README.md",

View file

@ -1,16 +0,0 @@
import { Flaska } from './flaska.mjs'
const port = 51024
const flaska = new Flaska({}, )
flaska.get('/', function(ctx) {
ctx.body = { status: true }
})
flaska.get('/error', function(ctx) {
process.exit(1)
})
flaska.listen(port, function() {
console.log('listening on port', port)
})

View file

@ -5,11 +5,11 @@ import { createCtx, fakeHttp } from './helper.mjs'
const faker = fakeHttp() const faker = fakeHttp()
t.describe('#constructor', function() { t.describe('#constructor', function() {
t.test('should be able to override the http', function() { t.test('should be able to override the http', function() {
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
assert.strictEqual(flaska.http, faker) assert.strictEqual(flaska.http, faker)
}) })
t.test('it should have all the common verbs', function() { t.test('it should have all the common verbs', function() {
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
assert.ok(flaska.get) assert.ok(flaska.get)
@ -25,7 +25,7 @@ t.describe('#constructor', function() {
assert.ok(flaska.patch) assert.ok(flaska.patch)
assert.strictEqual(typeof(flaska.patch), 'function') assert.strictEqual(typeof(flaska.patch), 'function')
}) })
t.test('the verbs GET and HEAD should be identical', function() { t.test('the verbs GET and HEAD should be identical', function() {
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
assert.ok(flaska.get) assert.ok(flaska.get)
@ -38,7 +38,7 @@ t.describe('#constructor', function() {
t.test('should have before default header generator', function() { t.test('should have before default header generator', function() {
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
assert.strictEqual(flaska._before.length, 1) assert.strictEqual(flaska._before.length, 1)
let ctx = {} let ctx = {}
flaska._before[0](ctx) flaska._before[0](ctx)
@ -78,7 +78,7 @@ t.describe('#constructor', function() {
let ctx = {} let ctx = {}
flaska._before[0](ctx) flaska._before[0](ctx)
let keys = Object.keys(defaultHeaders) let keys = Object.keys(defaultHeaders)
assert.strictEqual(Object.keys(ctx.headers).length, keys.length + 1) assert.strictEqual(Object.keys(ctx.headers).length, keys.length + 1)
@ -103,7 +103,7 @@ t.describe('#constructor', function() {
let ctx = {} let ctx = {}
flaska._before[0](ctx) flaska._before[0](ctx)
assert.deepEqual( assert.deepEqual(
Object.keys(ctx.headers).sort(), Object.keys(ctx.headers).sort(),
['Server', 'Herp', 'X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort() ['Server', 'Herp', 'X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort()
@ -140,7 +140,7 @@ t.describe('#_nonce', function() {
set.add(entry) set.add(entry)
}) })
assert.strictEqual(set.size, flaska._nonces.length) assert.strictEqual(set.size, flaska._nonces.length)
let ctx = createCtx() let ctx = createCtx()
assert.notOk(ctx.state.nonce) assert.notOk(ctx.state.nonce)
@ -148,12 +148,12 @@ t.describe('#_nonce', function() {
let oldIndex = flaska._noncesIndex let oldIndex = flaska._noncesIndex
flaska._before[0](ctx) flaska._before[0](ctx)
assert.ok(ctx.state.nonce) assert.ok(ctx.state.nonce)
assert.strictEqual(flaska._noncesIndex, oldIndex - 1) assert.strictEqual(flaska._noncesIndex, oldIndex - 1)
assert.strictEqual(flaska._nonces[oldIndex], ctx.state.nonce) assert.strictEqual(flaska._nonces[oldIndex], ctx.state.nonce)
assert.strictEqual(ctx.headers['Server'], 'Flaska') assert.strictEqual(ctx.headers['Server'], 'Flaska')
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff') assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`) assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`)
@ -162,7 +162,7 @@ t.describe('#_nonce', function() {
assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp') assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp')
assert.ok(new Date(ctx.headers['Date']).getDate()) assert.ok(new Date(ctx.headers['Date']).getDate())
}) })
t.test('should always return nonce values even if it runs out in cache', function() { t.test('should always return nonce values even if it runs out in cache', function() {
let flaska = new Flaska({ let flaska = new Flaska({
nonce: ['script-src'], nonce: ['script-src'],
@ -178,9 +178,9 @@ t.describe('#_nonce', function() {
} }
assert.notOk(flaska._nonces[flaska._noncesIndex]) assert.notOk(flaska._nonces[flaska._noncesIndex])
flaska._before[0](ctx) flaska._before[0](ctx)
assert.notOk(flaska._nonces[flaska._noncesIndex]) assert.notOk(flaska._nonces[flaska._noncesIndex])
assert.ok(ctx.state.nonce) assert.ok(ctx.state.nonce)
@ -206,13 +206,13 @@ t.describe('#_nonce', function() {
set.add(entry) set.add(entry)
}) })
assert.strictEqual(set.size, 5) assert.strictEqual(set.size, 5)
flaska._before[0](ctx) flaska._before[0](ctx)
flaska._before[0](ctx) flaska._before[0](ctx)
flaska._before[0](ctx) flaska._before[0](ctx)
assert.strictEqual(flaska._noncesIndex, 1) assert.strictEqual(flaska._noncesIndex, 1)
flaska._after[0](ctx) flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 2) assert.strictEqual(flaska._noncesIndex, 2)
set.add(flaska._nonces[flaska._noncesIndex]) set.add(flaska._nonces[flaska._noncesIndex])
@ -227,7 +227,7 @@ t.describe('#_nonce', function() {
assert.strictEqual(flaska._noncesIndex, 4) assert.strictEqual(flaska._noncesIndex, 4)
set.add(flaska._nonces[flaska._noncesIndex]) set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 8) assert.strictEqual(set.size, 8)
flaska._after[0](ctx) flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 4) assert.strictEqual(flaska._noncesIndex, 4)
set.add(flaska._nonces[flaska._noncesIndex]) set.add(flaska._nonces[flaska._noncesIndex])
@ -249,12 +249,12 @@ t.describe('#_nonce', function() {
set.add(entry) set.add(entry)
}) })
assert.strictEqual(set.size, 2) assert.strictEqual(set.size, 2)
flaska._before[0](ctx) flaska._before[0](ctx)
flaska._before[0](ctx) flaska._before[0](ctx)
assert.strictEqual(flaska._noncesIndex, -1) assert.strictEqual(flaska._noncesIndex, -1)
flaska._before[0](ctx) flaska._before[0](ctx)
assert.strictEqual(flaska._noncesIndex, -2) assert.strictEqual(flaska._noncesIndex, -2)
set.add(ctx.state.nonce) set.add(ctx.state.nonce)
@ -271,19 +271,19 @@ t.describe('#_nonce', function() {
assert.strictEqual(set.size, 5) assert.strictEqual(set.size, 5)
assert.strictEqual(Object.keys(flaska._nonces).length, 2) assert.strictEqual(Object.keys(flaska._nonces).length, 2)
flaska._after[0](ctx) flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 0) assert.strictEqual(flaska._noncesIndex, 0)
set.add(flaska._nonces[flaska._noncesIndex]) set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 6) assert.strictEqual(set.size, 6)
assert.strictEqual(Object.keys(flaska._nonces).length, 2) assert.strictEqual(Object.keys(flaska._nonces).length, 2)
flaska._after[0](ctx) flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 1) assert.strictEqual(flaska._noncesIndex, 1)
set.add(flaska._nonces[flaska._noncesIndex]) set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 7) assert.strictEqual(set.size, 7)
assert.strictEqual(Object.keys(flaska._nonces).length, 2) assert.strictEqual(Object.keys(flaska._nonces).length, 2)
flaska._after[0](ctx) flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 1) assert.strictEqual(flaska._noncesIndex, 1)
set.add(flaska._nonces[flaska._noncesIndex]) set.add(flaska._nonces[flaska._noncesIndex])
@ -299,7 +299,7 @@ t.describe('#log', function() {
assert.strictEqual(typeof(flaska.log.info), 'function') assert.strictEqual(typeof(flaska.log.info), 'function')
assert.strictEqual(typeof(flaska.log.warn), 'function') assert.strictEqual(typeof(flaska.log.warn), 'function')
}) })
t.test('allow overwriting in options', function() { t.test('allow overwriting in options', function() {
const assertFunction = function() { return 1 } const assertFunction = function() { return 1 }
let flaska = new Flaska({ log: { let flaska = new Flaska({ log: {
@ -323,7 +323,7 @@ specialHandlers.forEach(function(type) {
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
assert.strictEqual(typeof(flaska[type]), 'function') assert.strictEqual(typeof(flaska[type]), 'function')
}) })
t.test('validate handler', function() { t.test('validate handler', function() {
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
assert.throws(function() { flaska[type]() }, /[Ff]unction/) assert.throws(function() { flaska[type]() }, /[Ff]unction/)
@ -424,7 +424,7 @@ t.describe('_onerror', function() {
const assertStatus = 431 const assertStatus = 431
const assertBody = { a: 1 } const assertBody = { a: 1 }
const assertError = new HttpError(assertStatus, 'should not be seen', assertBody) const assertError = new HttpError(assertStatus, 'should not be seen', assertBody)
let ctx = createCtx() let ctx = createCtx()
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska._onerror(assertError, ctx) flaska._onerror(assertError, ctx)
@ -437,7 +437,7 @@ t.describe('_onerror', function() {
t.test('default valid handling of HttpError with no body', function() { t.test('default valid handling of HttpError with no body', function() {
const assertStatus = 413 const assertStatus = 413
const assertError = new HttpError(assertStatus, 'should not be seen') const assertError = new HttpError(assertStatus, 'should not be seen')
let ctx = createCtx() let ctx = createCtx()
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska._onerror(assertError, ctx) flaska._onerror(assertError, ctx)
@ -453,7 +453,7 @@ t.describe('_onerror', function() {
t.test('default valid handling of HttpError with missing status message', function() { t.test('default valid handling of HttpError with missing status message', function() {
const assertStatus = 432 const assertStatus = 432
const assertError = new HttpError(assertStatus, 'should not be seen') const assertError = new HttpError(assertStatus, 'should not be seen')
let ctx = createCtx() let ctx = createCtx()
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska._onerror(assertError, ctx) flaska._onerror(assertError, ctx)
@ -531,7 +531,7 @@ t.describe('#devMode()', function() {
const assertStatus = 431 const assertStatus = 431
const assertBody = { a: 1 } const assertBody = { a: 1 }
const assertError = new HttpError(assertStatus, 'should not be seen', assertBody) const assertError = new HttpError(assertStatus, 'should not be seen', assertBody)
let ctx = createCtx() let ctx = createCtx()
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.devMode() flaska.devMode()
@ -546,7 +546,7 @@ t.describe('#devMode()', function() {
const assertStatus = 413 const assertStatus = 413
const assertErrorMessage = 'A day' const assertErrorMessage = 'A day'
const assertError = new HttpError(assertStatus, assertErrorMessage) const assertError = new HttpError(assertStatus, assertErrorMessage)
let ctx = createCtx() let ctx = createCtx()
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.devMode() flaska.devMode()
@ -563,7 +563,7 @@ t.describe('#devMode()', function() {
const assertStatus = 432 const assertStatus = 432
const assertErrorMessage = 'Jet Coaster Ride' const assertErrorMessage = 'Jet Coaster Ride'
const assertError = new HttpError(assertStatus, assertErrorMessage) const assertError = new HttpError(assertStatus, assertErrorMessage)
let ctx = createCtx() let ctx = createCtx()
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.devMode() flaska.devMode()
@ -735,16 +735,16 @@ t.describe('#compile()', function() {
}) })
}) })
t.describe('#handleMiddleware()', function() { t.describe('#runHandlers()', function() {
t.test('should work with empty array', function() { t.test('should work with empty array', function() {
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.handleMiddleware({}, [], 0) flaska.runHandlers({}, [], 0)
}) })
t.test('should work with correct index', function() { t.test('should work with correct index', function() {
let checkIsTrue = false let checkIsTrue = false
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.handleMiddleware({}, [ flaska.runHandlers({}, [
function() { throw new Error('should not be thrown') }, function() { throw new Error('should not be thrown') },
function() { throw new Error('should not be thrown') }, function() { throw new Error('should not be thrown') },
function() { throw new Error('should not be thrown') }, function() { throw new Error('should not be thrown') },
@ -757,7 +757,7 @@ t.describe('#handleMiddleware()', function() {
const assertCtx = createCtx({ a: 1 }) const assertCtx = createCtx({ a: 1 })
let checkCounter = 0 let checkCounter = 0
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.handleMiddleware(assertCtx, [ flaska.runHandlers(assertCtx, [
function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ },
@ -771,7 +771,7 @@ t.describe('#handleMiddleware()', function() {
const assertCtx = createCtx({ a: 1 }) const assertCtx = createCtx({ a: 1 })
let checkCounter = 0 let checkCounter = 0
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
let result = flaska.handleMiddleware(assertCtx, [ let result = flaska.runHandlers(assertCtx, [
function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 0); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 0); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 1); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 1); checkCounter++ },
function(ctx) { return new Promise(function(res) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 2); checkCounter++; res() }) }, function(ctx) { return new Promise(function(res) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 2); checkCounter++; res() }) },
@ -793,19 +793,19 @@ t.describe('#handleMiddleware()', function() {
const assertError = { a: 1 } const assertError = { a: 1 }
let checkCounter = 0 let checkCounter = 0
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
let err = await assert.isRejected(flaska.handleMiddleware({}, [ let err = await assert.isRejected(flaska.runHandlers({}, [
function() { }, function() { },
function() { return new Promise(function(res, rej) { rej(assertError) }) }, function() { return new Promise(function(res, rej) { rej(assertError) }) },
function() { throw new Error('should not be seen') }, function() { throw new Error('should not be seen') },
], 0)) ], 0))
assert.strictEqual(err, assertError) assert.strictEqual(err, assertError)
err = await assert.isRejected(flaska.handleMiddleware({}, [ err = await assert.isRejected(flaska.runHandlers({}, [
function() { }, function() { },
function() { return Promise.reject(assertError) }, function() { return Promise.reject(assertError) },
function() { throw new Error('should not be seen') }, function() { throw new Error('should not be seen') },
], 0)) ], 0))
assert.strictEqual(err, assertError) assert.strictEqual(err, assertError)
err = await assert.isRejected(flaska.handleMiddleware({}, [ err = await assert.isRejected(flaska.runHandlers({}, [
function() { }, function() { },
function() { return Promise.resolve() }, function() { return Promise.resolve() },
function() { throw assertError }, function() { throw assertError },
@ -977,7 +977,7 @@ t.describe('#listenAsync()', function() {
assert.strictEqual(checkPort, assertPort) assert.strictEqual(checkPort, assertPort)
assert.strictEqual(checkIp, '::') assert.strictEqual(checkIp, '::')
}) })
t.test('call http and listenAsync correctly if supported', async function() { t.test('call http and listenAsync correctly if supported', async function() {
const assertPort = 4632 const assertPort = 4632
const assertIp = 'asdf' const assertIp = 'asdf'
@ -1002,7 +1002,7 @@ t.describe('#listenAsync()', function() {
assert.strictEqual(stubListenAsync.firstCall[0], assertPort) assert.strictEqual(stubListenAsync.firstCall[0], assertPort)
assert.strictEqual(stubListenAsync.firstCall[1], assertIp) assert.strictEqual(stubListenAsync.firstCall[1], assertIp)
}) })
t.test('call http and listenAsync correctly if supported and ip is null', async function() { t.test('call http and listenAsync correctly if supported and ip is null', async function() {
const assertPort = 325897235 const assertPort = 325897235
const assertReturns = { a: 1 } const assertReturns = { a: 1 }
@ -1072,10 +1072,23 @@ t.describe('#closeAsync()', function() {
assert.strictEqual(err, assertError) assert.strictEqual(err, assertError)
}) })
t.test('it should call closeAllConnections', async function() {
const assertError = new Error('Pirate Fight')
let flaska = new Flaska()
flaska.server = {
close: stub(),
closeAllConnections: stub(),
}
flaska.server.closeAllConnections.throws(assertError)
let err = await assert.isRejected(flaska.closeAsync())
assert.strictEqual(err, assertError)
})
t.test('should otherwise work', async function() { t.test('should otherwise work', async function() {
let flaska = new Flaska() let flaska = new Flaska()
flaska.server = { flaska.server = {
close: stub() close: stub(),
closeAllConnections: stub(),
} }
flaska.server.close.returnWith(function(cb) { flaska.server.close.returnWith(function(cb) {
cb(null, { a: 1 }) cb(null, { a: 1 })

View file

@ -30,7 +30,7 @@ t.describe('#requestStart()', function() {
assert.strictEqual(onResError.callCount, 1) assert.strictEqual(onResError.callCount, 1)
assert.strictEqual(onResError.firstCall[0], assertErrorTwo) assert.strictEqual(onResError.firstCall[0], assertErrorTwo)
assert.strictEqual(onResError.firstCall[1], ctx) assert.strictEqual(onResError.firstCall[1], ctx)
assert.strictEqual(assertRes.on.secondCall[0], 'finish') assert.strictEqual(assertRes.on.secondCall[0], 'finish')
assert.strictEqual(typeof(assertRes.on.secondCall[1]), 'function') assert.strictEqual(typeof(assertRes.on.secondCall[1]), 'function')
assert.strictEqual(onEnded.callCount, 0) assert.strictEqual(onEnded.callCount, 0)
@ -220,7 +220,7 @@ t.describe('#requestStart()', function() {
}), createRes()) }), createRes())
}) })
t.test('calls handleMiddleware correctly', function(cb) { t.test('calls handlers correctly', function(cb) {
const assertError = new Error('test') const assertError = new Error('test')
const assertMiddles = [1, 2] const assertMiddles = [1, 2]
const assertParams = { a: 1, b: 2 } const assertParams = { a: 1, b: 2 }
@ -231,12 +231,11 @@ t.describe('#requestStart()', function() {
flaska.routers.GET.match = function() { flaska.routers.GET.match = function() {
return { return {
handler: function() {}, path: { handlers: assertMiddles, },
middlewares: assertMiddles,
params: assertParams, params: assertParams,
} }
} }
flaska.handleMiddleware = function(ctx, middles, index) { flaska.runHandlers = function(ctx, middles, index) {
assert.strictEqual(index, 0) assert.strictEqual(index, 0)
assert.strictEqual(middles, assertMiddles) assert.strictEqual(middles, assertMiddles)
checkMiddleCtx = ctx checkMiddleCtx = ctx
@ -265,7 +264,7 @@ t.describe('#requestStart()', function() {
checkHandlerCtx = ctx checkHandlerCtx = ctx
throw assertError throw assertError
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.get('/:id', handler) flaska.get('/:id', handler)
flaska.compile() flaska.compile()
@ -289,7 +288,7 @@ t.describe('#requestStart()', function() {
let handler = function() { let handler = function() {
throw new Error('should not be called') throw new Error('should not be called')
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.on404(on404Error) flaska.on404(on404Error)
flaska.get('/test', function() { throw new Error('should not be called') }) flaska.get('/test', function() { throw new Error('should not be called') })
@ -307,7 +306,7 @@ t.describe('#requestStart()', function() {
method: 'GET', method: 'GET',
}), createRes()) }), createRes())
}) })
t.test('calls 404 if route handler is not found and supports promise', function(cb) { t.test('calls 404 if route handler is not found and supports promise', function(cb) {
let checkCtx = null let checkCtx = null
const assertError = new Error('should be seen') const assertError = new Error('should be seen')
@ -315,7 +314,7 @@ t.describe('#requestStart()', function() {
checkCtx = ctx checkCtx = ctx
return Promise.resolve().then(function() { return Promise.reject(assertError) }) return Promise.resolve().then(function() { return Promise.reject(assertError) })
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.on404(on404Error) flaska.on404(on404Error)
flaska.get('/test', function() { throw new Error('should not be called') }) flaska.get('/test', function() { throw new Error('should not be called') })
@ -336,7 +335,7 @@ t.describe('#requestStart()', function() {
t.test(`should handle unexpected errors in on404 correctly`, function(cb) { t.test(`should handle unexpected errors in on404 correctly`, function(cb) {
const assertError = new Error('should be seen') const assertError = new Error('should be seen')
let checkCtx = null let checkCtx = null
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.on404(function(ctx) { flaska.on404(function(ctx) {
checkCtx = ctx checkCtx = ctx
@ -360,7 +359,7 @@ t.describe('#requestStart()', function() {
t.test(`should handle unexpected errors in middleware correctly`, function(cb) { t.test(`should handle unexpected errors in middleware correctly`, function(cb) {
const assertError = new Error('should be seen') const assertError = new Error('should be seen')
let checkCtx = null let checkCtx = null
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
let middles = [function(ctx) { let middles = [function(ctx) {
checkCtx = ctx checkCtx = ctx
@ -387,13 +386,10 @@ t.describe('#requestStart()', function() {
assert.strictEqual(ctx.params.path, 'test/something/here') assert.strictEqual(ctx.params.path, 'test/something/here')
ctx.body = assertBody ctx.body = assertBody
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.get('/::path', handler) flaska.get('/::path', handler)
flaska.compile() flaska.compile()
flaska.handleMiddleware = function() {
throw new Error('should not be called')
}
flaska.requestEnd = cb.finish(function(err, ctx) { flaska.requestEnd = cb.finish(function(err, ctx) {
assert.notOk(err) assert.notOk(err)
@ -407,7 +403,7 @@ t.describe('#requestStart()', function() {
}), createRes()) }), createRes())
}) })
t.test('calls handleMiddleware correctly if is promise', function(cb) { t.test('calls runHandlers correctly if is promise', function(cb) {
const assertError = new Error('test') const assertError = new Error('test')
const assertMiddles = [1] const assertMiddles = [1]
@ -416,11 +412,11 @@ t.describe('#requestStart()', function() {
flaska.routers.GET.match = function() { flaska.routers.GET.match = function() {
return { return {
handler: function() {}, path: { handlers: function() {}, },
middlewares: assertMiddles, middlewares: assertMiddles,
} }
} }
flaska.handleMiddleware = function() { flaska.runHandlers = function() {
return Promise.resolve().then(function() { return Promise.reject(assertError) }) return Promise.resolve().then(function() { return Promise.reject(assertError) })
} }
@ -444,7 +440,7 @@ t.describe('#requestStart()', function() {
checkHandlerCtx = ctx checkHandlerCtx = ctx
throw assertError throw assertError
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.get('/:id', [function() { return Promise.resolve() }], handler) flaska.get('/:id', [function() { return Promise.resolve() }], handler)
flaska.compile() flaska.compile()
@ -470,7 +466,7 @@ t.describe('#requestStart()', function() {
checkHandlerCtx = ctx checkHandlerCtx = ctx
return Promise.resolve().then(function() { return Promise.reject(assertError) }) return Promise.resolve().then(function() { return Promise.reject(assertError) })
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.get('/:id', [function() { return Promise.resolve() }], handler) flaska.get('/:id', [function() { return Promise.resolve() }], handler)
flaska.compile() flaska.compile()
@ -504,7 +500,7 @@ t.describe('#requestStart()', function() {
ctx.body = assertBody ctx.body = assertBody
}) })
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.get('/::path', [middle], handler) flaska.get('/::path', [middle], handler)
flaska.compile() flaska.compile()
@ -521,7 +517,7 @@ t.describe('#requestStart()', function() {
method: 'GET', method: 'GET',
}), createRes()) }), createRes())
}) })
t.test('calls route handler correctly if promise', function(cb) { t.test('calls route handler correctly if promise', function(cb) {
const assertError = new Error('test') const assertError = new Error('test')
let checkHandlerCtx = null let checkHandlerCtx = null
@ -532,7 +528,7 @@ t.describe('#requestStart()', function() {
return Promise.reject(assertError) return Promise.reject(assertError)
}) })
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.get('/:id', handler) flaska.get('/:id', handler)
flaska.compile() flaska.compile()
@ -558,15 +554,11 @@ t.describe('#requestStart()', function() {
ctx.body = assertBody ctx.body = assertBody
}) })
} }
let flaska = new Flaska({}, faker) let flaska = new Flaska({}, faker)
flaska.get('/::path', [], handler) flaska.get('/::path', [], handler)
flaska.compile() flaska.compile()
flaska.handleMiddleware = function() {
throw new Error('should not be called')
}
flaska.requestEnd = cb.finish(function(err, ctx) { flaska.requestEnd = cb.finish(function(err, ctx) {
assert.notOk(err) assert.notOk(err)
assert.ok(ctx) assert.ok(ctx)

View file

@ -0,0 +1,128 @@
import { Eltro as t, assert, stub } from 'eltro'
import { setTimeout } from 'timers/promises'
import { Flaska, JsonHandler } from '../flaska.mjs'
import Client from './client.mjs'
const port = 51024
const log = {
fatal: stub(),
error: stub(),
warn: stub(),
info: stub(),
debug: stub(),
trace: stub(),
log: stub(),
}
const flaska = new Flaska({ log })
const client = new Client(port)
let reqBody = null
flaska.after(function(ctx) {
if (ctx.aborted) return
ctx.log.info(ctx.status)
})
flaska.post('/json', JsonHandler(), function(ctx) {
ctx.body = { success: true }
reqBody = ctx.req.body
})
flaska.post('/json/slow', JsonHandler(), async function(ctx) {
await setTimeout(300)
ctx.body = { success: true }
ctx.status = 201
reqBody = ctx.req.body
})
function reset() {
log.fatal.reset()
log.error.reset()
log.warn.reset()
log.info.reset()
log.debug.reset()
log.trace.reset()
log.log.reset()
}
t.before(function() {
return flaska.listenAsync(port)
})
t.describe('/json', function() {
t.beforeEach(function() {
log.info.reset()
})
t.test('should return success and store body', async function() {
const assertBody = { a: '' }
for (let i = 0; i < 1010; i++) {
assertBody.a += 'aaaaaaaaaa'
}
reqBody = null
let body = await client.post('/json', assertBody)
assert.deepEqual(body, { success: true })
assert.deepStrictEqual(reqBody, assertBody)
assert.strictEqual(log.info.callCount, 1)
assert.strictEqual(log.info.firstCall[0], 200)
})
t.test('should return and log correctly', async function() {
const assertBody = { a: '' }
for (let i = 0; i < 1010; i++) {
assertBody.a += 'aaaaaaaaaa'
}
reqBody = null
let body = await client.post('/json/slow', assertBody)
assert.deepEqual(body, { success: true })
assert.deepStrictEqual(reqBody, assertBody)
assert.strictEqual(log.info.callCount, 1)
assert.strictEqual(log.info.firstCall[0], 201)
})
t.test('should fail if body is too big', async function() {
reset()
const assertBody = { a: '' }
for (let i = 0; i < 10300; i++) {
assertBody.a += 'aaaaaaaaaa'
}
let err = await assert.isRejected(client.post('/json', assertBody))
assert.strictEqual(err.body.status, 413)
assert.ok(log.error.called)
assert.match(log.error.firstCall[0].message, /10240/)
assert.strictEqual(log.error.firstCall[0].status, 413)
})
t.test('should fail if not a valid json', async function() {
reset()
let err = await assert.isRejected(client.customRequest('POST', '/json', 'XXXXX'))
assert.strictEqual(err.body.status, 400)
assert.match(err.body.message, /invalid json/i)
assert.match(err.body.message, /token[^X]+X/i)
assert.strictEqual(err.body.request, 'XXXXX')
assert.strictEqual(log.error.callCount, 1)
assert.match(log.error.firstCall[0].message, /invalid json/i)
assert.match(log.error.firstCall[0].message, /token[^X]+X/i)
})
t.test('should handle incomplete requests correctly', async function() {
reset()
let req = await client.customRequest('POST', '/json', 'aaaa', { returnRequest: true })
req.write('part1')
await setTimeout(20)
req.destroy()
await setTimeout(20)
assert.strictEqual(log.error.callCount, 0)
assert.strictEqual(log.info.callCount, 0)
// assert.strictEqual(log.info.firstCall[0].message, 'aborted')
})
})

View file

@ -4,10 +4,10 @@ import fs from 'fs/promises'
import formidable from 'formidable' import formidable from 'formidable'
import { Eltro as t, assert, stub } from 'eltro' import { Eltro as t, assert, stub } from 'eltro'
import { setTimeout } from 'timers/promises' import { setTimeout } from 'timers/promises'
import { Flaska, JsonHandler, FormidableHandler, FileResponse } from '../flaska.mjs' import { Flaska, FormidableHandler, FileResponse } from '../flaska.mjs'
import Client from './client.mjs' import Client from './client.mjs'
const port = 51024 const port = 51025
const log = { const log = {
fatal: stub(), fatal: stub(),
error: stub(), error: stub(),
@ -20,7 +20,6 @@ const log = {
const flaska = new Flaska({ log }) const flaska = new Flaska({ log })
const client = new Client(port) const client = new Client(port)
let reqBody = null
let file = null let file = null
let uploaded = [] let uploaded = []
@ -32,16 +31,6 @@ flaska.after(function(ctx) {
flaska.get('/', function(ctx) { flaska.get('/', function(ctx) {
ctx.body = { status: true } ctx.body = { status: true }
}) })
flaska.post('/json', JsonHandler(), function(ctx) {
ctx.body = { success: true }
reqBody = ctx.req.body
})
flaska.post('/json/slow', JsonHandler(), async function(ctx) {
await setTimeout(300)
ctx.body = { success: true }
ctx.status = 201
reqBody = ctx.req.body
})
flaska.get('/timeout', function(ctx) { flaska.get('/timeout', function(ctx) {
return new Promise(function() {}) return new Promise(function() {})
}) })
@ -96,90 +85,11 @@ t.describe('/', function() {
}) })
}) })
t.describe('/json', function() {
t.beforeEach(function() {
log.info.reset()
})
t.test('should return success and store body', async function() {
const assertBody = { a: '' }
for (let i = 0; i < 1010; i++) {
assertBody.a += 'aaaaaaaaaa'
}
reqBody = null
let body = await client.post('/json', assertBody)
assert.deepEqual(body, { success: true })
assert.deepStrictEqual(reqBody, assertBody)
assert.strictEqual(log.info.callCount, 1)
assert.strictEqual(log.info.firstCall[0], 200)
})
t.test('should return and log correctly', async function() {
const assertBody = { a: '' }
for (let i = 0; i < 1010; i++) {
assertBody.a += 'aaaaaaaaaa'
}
reqBody = null
let body = await client.post('/json/slow', assertBody)
assert.deepEqual(body, { success: true })
assert.deepStrictEqual(reqBody, assertBody)
assert.strictEqual(log.info.callCount, 1)
assert.strictEqual(log.info.firstCall[0], 201)
})
t.test('should fail if body is too big', async function() {
reset()
const assertBody = { a: '' }
for (let i = 0; i < 10300; i++) {
assertBody.a += 'aaaaaaaaaa'
}
let err = await assert.isRejected(client.post('/json', assertBody))
assert.strictEqual(err.body.status, 413)
assert.ok(log.error.called)
assert.match(log.error.firstCall[0].message, /10240/)
assert.strictEqual(log.error.firstCall[0].status, 413)
})
t.test('should fail if not a valid json', async function() {
reset()
let err = await assert.isRejected(client.customRequest('POST', '/json', 'XXXXX'))
assert.strictEqual(err.body.status, 400)
assert.match(err.body.message, /invalid json/i)
assert.match(err.body.message, /token[^X]+X/i)
assert.strictEqual(err.body.request, 'XXXXX')
assert.strictEqual(log.error.callCount, 1)
assert.match(log.error.firstCall[0].message, /invalid json/i)
assert.match(log.error.firstCall[0].message, /token[^X]+X/i)
})
t.test('should handle incomplete requests correctly', async function() {
reset()
let req = await client.customRequest('POST', '/json', 'aaaa', { returnRequest: true })
req.write('part1')
await setTimeout(20)
req.destroy()
await setTimeout(20)
assert.strictEqual(log.error.callCount, 0)
assert.strictEqual(log.info.callCount, 0)
// assert.strictEqual(log.info.firstCall[0].message, 'aborted')
})
})
t.describe('/timeout', function() { t.describe('/timeout', function() {
t.test('server should handle timeout', async function() { t.test('server should handle timeout', async function() {
reset() reset()
let err = await assert.isRejected(client.customRequest('GET', '/timeout', JSON.stringify({}), { timeout: 20 })) let err = await assert.isRejected(client.customRequest('GET', '/timeout', null, { timeout: 20 }))
assert.match(err.message, /timed out/) assert.match(err.message, /timed out/)
assert.notOk(log.error.called) assert.notOk(log.error.called)
@ -201,7 +111,7 @@ t.describe('/file', function() {
assert.ok(target.closed) assert.ok(target.closed)
assert.ok(file.closed) assert.ok(file.closed)
let [statSource, statTarget] = await Promise.all([ let [statSource, statTarget] = await Promise.all([
fs.stat('./test/test.png'), fs.stat('./test/test.png'),
fs.stat('./test_tmp.png'), fs.stat('./test_tmp.png'),
@ -226,11 +136,11 @@ t.describe('/file', function() {
assert.notOk(file.closed) assert.notOk(file.closed)
req.destroy() req.destroy()
while (!file.closed) { while (!file.closed) {
await setTimeout(10) await setTimeout(10)
} }
assert.strictEqual(log.error.callCount, 0) assert.strictEqual(log.error.callCount, 0)
assert.strictEqual(log.info.callCount, 0) assert.strictEqual(log.info.callCount, 0)
@ -285,7 +195,7 @@ t.describe('/filehandle', function() {
assert.strictEqual(res.data, 'llo ') assert.strictEqual(res.data, 'llo ')
assert.strictEqual(res.headers['content-length'], '4') assert.strictEqual(res.headers['content-length'], '4')
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent, res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: { headers: {
'Range': 'bytes=0-0' 'Range': 'bytes=0-0'
@ -401,15 +311,15 @@ t.describe('HEAD', function() {
t.describe('/file', function() { t.describe('/file', function() {
t.test('server return HEAD for pipes', async function() { t.test('server return HEAD for pipes', async function() {
log.error.reset() log.error.reset()
let res = await client.customRequest('HEAD', '/file', null, { getRaw: true, agent: agent }) let res = await client.customRequest('HEAD', '/file', null, { getRaw: true, agent: agent })
while (!file.closed) { while (!file.closed) {
await setTimeout(10) await setTimeout(10)
} }
assert.ok(file.closed) assert.ok(file.closed)
let statSource = await fs.stat('./test/test.png') let statSource = await fs.stat('./test/test.png')
assert.strictEqual(res.data, '') assert.strictEqual(res.data, '')
@ -418,37 +328,37 @@ t.describe('HEAD', function() {
}) })
t.test('server should autoclose body file handles on errors', async function() { t.test('server should autoclose body file handles on errors', async function() {
reset() reset()
file = null file = null
let req = await client.customRequest('HEAD', '/file/leak', null, { returnRequest: true }) let req = await client.customRequest('HEAD', '/file/leak', null, { returnRequest: true })
req.end() req.end()
while (!file) { while (!file) {
await setTimeout(10) await setTimeout(10)
} }
assert.ok(file) assert.ok(file)
assert.notOk(file.closed) assert.notOk(file.closed)
req.destroy() req.destroy()
while (!file.closed) { while (!file.closed) {
await setTimeout(10) await setTimeout(10)
} }
assert.strictEqual(log.error.callCount, 0) assert.strictEqual(log.error.callCount, 0)
assert.strictEqual(log.info.callCount, 0) assert.strictEqual(log.info.callCount, 0)
assert.ok(file.closed) assert.ok(file.closed)
}) })
}) })
t.describe('/filehandle', function() { t.describe('/filehandle', function() {
t.test('server should send correctly', async function() { t.test('server should send correctly', async function() {
log.error.reset() log.error.reset()
let res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent }) let res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent })
assert.strictEqual(res.status, 200) assert.strictEqual(res.status, 200)
assert.strictEqual(res.headers['content-length'], '11') assert.strictEqual(res.headers['content-length'], '11')
@ -459,7 +369,7 @@ t.describe('HEAD', function() {
let etag = res.headers['etag'] let etag = res.headers['etag']
assert.ok(d.getTime()) assert.ok(d.getTime())
assert.strictEqual(res.data, '') assert.strictEqual(res.data, '')
res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent,
headers: { headers: {
'If-Modified-Since': d.toUTCString() 'If-Modified-Since': d.toUTCString()
@ -468,7 +378,7 @@ t.describe('HEAD', function() {
assert.strictEqual(res.status, 304) assert.strictEqual(res.status, 304)
assert.strictEqual(res.data, '') assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['etag'], etag) assert.strictEqual(res.headers['etag'], etag)
res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent,
headers: { headers: {
'If-None-Match': etag 'If-None-Match': etag
@ -477,7 +387,7 @@ t.describe('HEAD', function() {
assert.strictEqual(res.status, 304) assert.strictEqual(res.status, 304)
assert.strictEqual(res.data, '') assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['etag'], etag) assert.strictEqual(res.headers['etag'], etag)
res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent,
headers: { headers: {
'Range': 'bytes=2-5' 'Range': 'bytes=2-5'
@ -486,8 +396,8 @@ t.describe('HEAD', function() {
assert.strictEqual(res.status, 206) assert.strictEqual(res.status, 206)
assert.strictEqual(res.data, '') assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['content-length'], '4') assert.strictEqual(res.headers['content-length'], '4')
res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent,
headers: { headers: {
'Range': 'bytes=0-0' 'Range': 'bytes=0-0'
@ -497,7 +407,7 @@ t.describe('HEAD', function() {
assert.strictEqual(res.data, '') assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['content-length'], '1') assert.strictEqual(res.headers['content-length'], '1')
}) })
t.after(function() { t.after(function() {
agent.destroy() agent.destroy()
}) })

View file

@ -9,7 +9,7 @@ t.describe('#addRoute()', function() {
assert.throws(function() { router.addRoute(':test') }, /forward slash/) assert.throws(function() { router.addRoute(':test') }, /forward slash/)
assert.throws(function() { router.addRoute('test/test2') }, /forward slash/) assert.throws(function() { router.addRoute('test/test2') }, /forward slash/)
}) })
t.test('fail if missing handler', function() { t.test('fail if missing handler', function() {
let router = new FlaskaRouter() let router = new FlaskaRouter()
assert.throws(function() { router.addRoute('/') }, /handler/) assert.throws(function() { router.addRoute('/') }, /handler/)
@ -45,8 +45,21 @@ t.describe('#addRoute()', function() {
router.addRoute('/:test/bla', function() {}) router.addRoute('/:test/bla', function() {})
router.addRoute('/bla/bla', function() {}) router.addRoute('/bla/bla', function() {})
router.addRoute('/bla/bla/bla', function() {}) router.addRoute('/bla/bla/bla', function() {})
assert.throws(function() { router.addRoute('/:asdf/', function() {}) }, /param/) assert.throws(function() { router.addRoute('/:asdf', function() {}) }, /existing/)
assert.throws(function() { router.addRoute('/:test/asdf/:foobar', function() {}) }, /param/) assert.throws(function() { router.addRoute('/:test/asdf/:foobar', function() {}) }, /existing/)
})
t.test('fail if adding anything after a fullparam', function() {
let router = new FlaskaRouter()
assert.throws(function() { router.addRoute('/::bla/:bla', function() {}) }, /after/)
assert.throws(function() { router.addRoute('/:test/::bla/test', function() {}) }, /after/)
assert.throws(function() { router.addRoute('/::test/bla', function() {}) }, /after/)
})
t.test('should work with param and full param side by side', function() {
let router = new FlaskaRouter()
router.addRoute('/:bla', function() {})
router.addRoute('/::bla', function() {})
}) })
t.test('fail if adding multiple fullparam', function() { t.test('fail if adding multiple fullparam', function() {
@ -56,10 +69,8 @@ t.describe('#addRoute()', function() {
router.addRoute('/:test/bla', function() {}) router.addRoute('/:test/bla', function() {})
router.addRoute('/:test/::bla', function() {}) router.addRoute('/:test/::bla', function() {})
router.addRoute('/bla/bla/bla', function() {}) router.addRoute('/bla/bla/bla', function() {})
assert.throws(function() { router.addRoute('/:test/asdf/::bla/fail', function() {}) }, /full.+param/) assert.throws(function() { router.addRoute('/:test/asdf/::bla', function() {}) }, /existing/)
assert.throws(function() { router.addRoute('/:test/::bla/test', function() {}) }, /full.+param/) assert.throws(function() { router.addRoute('/:test/::bla', function() {}) }, /existing/)
assert.throws(function() { router.addRoute('/:test/:bla', function() {}) }, /full.+param/)
assert.throws(function() { router.addRoute('/::test', function() {}) }, /partial.+param/)
}) })
t.test('add route correctly', function() { t.test('add route correctly', function() {
@ -67,39 +78,7 @@ t.describe('#addRoute()', function() {
let router = new FlaskaRouter() let router = new FlaskaRouter()
router.addRoute('/a/b/c', assertHandler) router.addRoute('/a/b/c', assertHandler)
let result = router.match('/a/b/c') let result = router.match('/a/b/c')
assert.strictEqual(result.handler, assertHandler) assert.strictEqual(result.path.handlers[0], assertHandler)
})
t.test('add param route correctly', function() {
let assertHandler = function() { return 1 }
let router = new FlaskaRouter()
router.addRoute('/a/:b/c', assertHandler)
assert.ok(router.root.children.get('a'))
assert.ok(router.root.children.get('a').children.get('__param'))
assert.strictEqual(router.root.children.get('a').children.get('__param').paramName, 'b')
assert.ok(router.root.children.get('a').children.get('__param').children.get('c'))
assert.strictEqual(router.root.children.get('a').children.get('__param').children.get('c').handler, assertHandler)
})
t.test('add full param route correctly', function() {
let assertHandler = function() { return 1 }
let router = new FlaskaRouter()
router.addRoute('/a/::b', assertHandler)
assert.ok(router.root.children.get('a'))
assert.ok(router.root.children.get('a').children.get('__fullparam'))
assert.strictEqual(router.root.children.get('a').children.get('__fullparam').fullparamName, 'b')
assert.strictEqual(router.root.children.get('a').children.get('__fullparam').handler, assertHandler)
})
t.test('add param route correctly', function() {
let assertHandler = function() { return 1 }
let router = new FlaskaRouter()
router.addRoute('/a/:b/c', assertHandler)
assert.ok(router.root.children.get('a'))
assert.ok(router.root.children.get('a').children.get('__param'))
assert.strictEqual(router.root.children.get('a').children.get('__param').paramName, 'b')
assert.ok(router.root.children.get('a').children.get('__param').children.get('c'))
assert.strictEqual(router.root.children.get('a').children.get('__param').children.get('c').handler, assertHandler)
}) })
t.test('support single middlewares correctly', function() { t.test('support single middlewares correctly', function() {
@ -107,10 +86,10 @@ t.describe('#addRoute()', function() {
let assertMiddleware = function() { return 1 } let assertMiddleware = function() { return 1 }
let router = new FlaskaRouter() let router = new FlaskaRouter()
router.addRoute('/a', assertMiddleware, assertHandler) router.addRoute('/a', assertMiddleware, assertHandler)
assert.ok(router.root.children.get('a')) let result = router.match('/a')
assert.strictEqual(router.root.children.get('a').handler, assertHandler) assert.strictEqual(result.path.handlers.length, 2)
assert.strictEqual(router.root.children.get('a').middlewares.length, 1) assert.strictEqual(result.path.handlers[0], assertMiddleware)
assert.strictEqual(router.root.children.get('a').middlewares[0], assertMiddleware) assert.strictEqual(result.path.handlers[1], assertHandler)
}) })
t.test('support multi middlewares correctly', function() { t.test('support multi middlewares correctly', function() {
@ -119,15 +98,15 @@ t.describe('#addRoute()', function() {
let router = new FlaskaRouter() let router = new FlaskaRouter()
router.addRoute('/a', [assertMiddleware], assertHandler) router.addRoute('/a', [assertMiddleware], assertHandler)
router.addRoute('/b', [assertMiddleware, assertMiddleware], assertHandler) router.addRoute('/b', [assertMiddleware, assertMiddleware], assertHandler)
assert.ok(router.root.children.get('a')) let resultA = router.match('/a')
assert.strictEqual(router.root.children.get('a').handler, assertHandler) assert.strictEqual(resultA.path.handlers.length, 2)
assert.strictEqual(router.root.children.get('a').middlewares.length, 1) assert.strictEqual(resultA.path.handlers[0], assertMiddleware)
assert.strictEqual(router.root.children.get('a').middlewares[0], assertMiddleware) assert.strictEqual(resultA.path.handlers[1], assertHandler)
assert.ok(router.root.children.get('b')) let resultB = router.match('/b')
assert.strictEqual(router.root.children.get('b').handler, assertHandler) assert.strictEqual(resultB.path.handlers.length, 3)
assert.strictEqual(router.root.children.get('b').middlewares.length, 2) assert.strictEqual(resultB.path.handlers[0], assertMiddleware)
assert.strictEqual(router.root.children.get('b').middlewares[0], assertMiddleware) assert.strictEqual(resultB.path.handlers[1], assertMiddleware)
assert.strictEqual(router.root.children.get('b').middlewares[1], assertMiddleware) assert.strictEqual(resultB.path.handlers[2], assertHandler)
}) })
}) })
@ -136,94 +115,47 @@ t.describe('#match()', function() {
let assertMatched = false let assertMatched = false
let router = new FlaskaRouter() let router = new FlaskaRouter()
router.addRoute('/test', function() { assertMatched = true }) router.addRoute('/test', function() { assertMatched = true })
let result = router.match('/test') let result = router.match('/test').path
assert.ok(result.handler) assert.strictEqual(result.handlers.length, 1)
assert.ok(result.middlewares) result.handlers[0]()
assert.strictEqual(result.middlewares.length, 0)
result.handler()
assert.strictEqual(assertMatched, true)
// Test with extra slash at the end
assertMatched = false
result = router.match('/test/')
assert.ok(result.handler)
assert.ok(result.middlewares)
assert.strictEqual(result.middlewares.length, 0)
result.handler()
assert.strictEqual(assertMatched, true) assert.strictEqual(assertMatched, true)
}) })
t.test('return middlewares in handlers', function() {
t.test('return middlewares', function() {
let assertMatched = false let assertMatched = false
let assertMiddleware = function() { assertMatched = true } let assertMiddleware = function() { assertMatched = true }
let router = new FlaskaRouter() let router = new FlaskaRouter()
router.addRoute('/test', assertMiddleware, function() { }) router.addRoute('/test', assertMiddleware, function() { })
let result = router.match('/test') let result = router.match('/test').path
assert.ok(result.handler) assert.strictEqual(result.handlers.length, 2)
assert.ok(result.middlewares) result.handlers[0]()
assert.strictEqual(result.middlewares.length, 1)
result.middlewares[0]()
assert.strictEqual(assertMatched, true)
// Test with extra slash at the end
assertMatched = false
result = router.match('/test/')
assert.ok(result.handler)
assert.ok(result.middlewares)
assert.strictEqual(result.middlewares.length, 1)
result.middlewares[0]()
assert.strictEqual(assertMatched, true) assert.strictEqual(assertMatched, true)
}) })
t.test('match variable paths', function() { t.test('match variable paths', function() {
const assertParameter = 'bla' const assertParameter = 'bla'
let assertMatched = false let assertMatched = false
let router = new FlaskaRouter() let router = new FlaskaRouter()
router.addRoute('/test/:id', function() { assertMatched = true }) router.addRoute('/test/:id', function() { assertMatched = true })
let result = router.match('/test/' + assertParameter) let result = router.match('/test/' + assertParameter)
assert.ok(result.handler) assert.strictEqual(result.path.handlers.length, 1)
assert.ok(result.middlewares) result.path.handlers[0]()
assert.strictEqual(result.middlewares.length, 0)
result.handler()
assert.strictEqual(assertMatched, true)
assert.strictEqual(result.params.id, assertParameter)
// Test with extra slash at the end
assertMatched = false
result = router.match('/test/' + assertParameter + '/')
assert.ok(result.handler)
assert.ok(result.middlewares)
assert.strictEqual(result.middlewares.length, 0)
result.handler()
assert.strictEqual(assertMatched, true) assert.strictEqual(assertMatched, true)
assert.strictEqual(result.params.id, assertParameter) assert.strictEqual(result.params.id, assertParameter)
}) })
t.test('match full path variable paths', function() { t.test('match full path variable paths', function() {
const assertParameter = 'bla/bla/bla' const assertParameter = 'bla/bla/bla'
let assertMatched = false let assertMatched = false
let router = new FlaskaRouter() let router = new FlaskaRouter()
router.addRoute('/test/::id', function() { assertMatched = true }) router.addRoute('/test/::id', function() { assertMatched = true })
let result = router.match('/test/' + assertParameter) let result = router.match('/test/' + assertParameter)
assert.ok(result.handler) assert.strictEqual(result.path.handlers.length, 1)
assert.ok(result.middlewares) result.path.handlers[0]()
assert.strictEqual(result.middlewares.length, 0)
result.handler()
assert.strictEqual(assertMatched, true)
assert.strictEqual(result.params.id, assertParameter)
// Test with extra slash at the end
assertMatched = false
result = router.match('/test/' + assertParameter + '/')
assert.ok(result.handler)
assert.ok(result.middlewares)
assert.strictEqual(result.middlewares.length, 0)
result.handler()
assert.strictEqual(assertMatched, true) assert.strictEqual(assertMatched, true)
assert.strictEqual(result.params.id, assertParameter) assert.strictEqual(result.params.id, assertParameter)
}) })
t.test('match full path root path properly', function() { t.test('match full path root path properly', function() {
const assertParamFunc = function() { } const assertParamFunc = function() { }
const assertFullFunc = function() { } const assertFullFunc = function() { }
@ -231,15 +163,13 @@ t.describe('#match()', function() {
router.addRoute('/test/:bla', assertParamFunc) router.addRoute('/test/:bla', assertParamFunc)
router.addRoute('/::id', assertFullFunc) router.addRoute('/::id', assertFullFunc)
let result = router.match('/test/123') let result = router.match('/test/123')
assert.strictEqual(result.handler, assertParamFunc) assert.strictEqual(result.path.handlers.length, 1)
assert.ok(result.middlewares) assert.strictEqual(result.path.handlers[0], assertParamFunc)
assert.strictEqual(result.middlewares.length, 0)
assert.strictEqual(result.params.bla, '123') assert.strictEqual(result.params.bla, '123')
result = router.match('/test/123/asdf') result = router.match('/test/123/asdf')
assert.strictEqual(result.handler, assertFullFunc) assert.strictEqual(result.path.handlers.length, 1)
assert.ok(result.middlewares) assert.strictEqual(result.path.handlers[0], assertFullFunc)
assert.strictEqual(result.middlewares.length, 0)
assert.strictEqual(result.params.id, 'test/123/asdf') assert.strictEqual(result.params.id, 'test/123/asdf')
assert.notOk(result.params.bla) assert.notOk(result.params.bla)
}) })
@ -250,19 +180,8 @@ t.describe('#match()', function() {
router.addRoute('/test/:id', function() { assertMatched = false }) router.addRoute('/test/:id', function() { assertMatched = false })
router.addRoute('/test/:id/test1', function() { }) router.addRoute('/test/:id/test1', function() { })
let result = router.match('/test/asdf/test1') let result = router.match('/test/asdf/test1')
assert.ok(result.handler) assert.strictEqual(result.path.handlers.length, 1)
assert.ok(result.middlewares) result.path.handlers[0]()
assert.strictEqual(result.middlewares.length, 0)
result.handler()
assert.strictEqual(assertMatched, true)
assert.strictEqual(result.params.id, 'asdf')
// Test with extra slash at the end
result = router.match('/test/asdf/test1/')
assert.ok(result.handler)
assert.ok(result.middlewares)
assert.strictEqual(result.middlewares.length, 0)
result.handler()
assert.strictEqual(assertMatched, true) assert.strictEqual(assertMatched, true)
assert.strictEqual(result.params.id, 'asdf') assert.strictEqual(result.params.id, 'asdf')
}) })
@ -277,24 +196,25 @@ t.describe('#match()', function() {
router.addRoute('/foo/::path', assertFunction) router.addRoute('/foo/::path', assertFunction)
router.addRoute('/::path', assertFailFunction) router.addRoute('/::path', assertFailFunction)
assert.strictEqual(router.match('/test/123').handler, assertFunction) assert.strictEqual(router.match('/test/123').path.handlers[0], assertFunction)
assert.strictEqual(router.match('/test/asdfasdg').handler, assertFunction) assert.strictEqual(router.match('/test/asdfasdg').path.handlers[0], assertFunction)
assert.strictEqual(router.match('/test/test/sdafsda').handler, assertFunction) assert.strictEqual(router.match('/test/test/sdafsda').path.handlers[0], assertFunction)
assert.strictEqual(router.match('/test/test/sdafsda/gdfshe4/43y34/wtaw').handler, assertFunction) assert.strictEqual(router.match('/test/test/sdafsda/gdfshe4/43y34/wtaw').path.handlers[0], assertFunction)
assert.strictEqual(router.match('/foo/123').handler, assertFunction) assert.strictEqual(router.match('/foo/123').path.handlers[0], assertFunction)
assert.strictEqual(router.match('/foo/bar/baz/test').handler, assertFunction) assert.strictEqual(router.match('/foo/bar/baz/test').path.handlers[0], assertFunction)
assert.ok(router.match('/test/123/yweherher/reher/h34h34/')) assert.ok(router.match('/test/123/yweherher/reher/h34h34/'))
assert.strictEqual(router.match('/test/123/yweherher/reher/h34h34/').handler, assertFailFunction) assert.strictEqual(router.match('/test/123/yweherher/reher/h34h34/').path.handlers[0], assertFailFunction)
assert.ok(router.match('/test/foo/bar')) assert.ok(router.match('/test/foo/bar'))
assert.strictEqual(router.match('/test/foo/bar').handler, assertFailFunction) assert.strictEqual(router.match('/test/foo/bar').path.handlers[0], assertFailFunction)
assert.ok(router.match('/')) assert.ok(router.match('/'))
assert.strictEqual(router.match('/').handler, assertFailFunction) assert.strictEqual(router.match('/').path.handlers[0], assertFailFunction)
assert.ok(router.match('/something/else/goes/here')) assert.ok(router.match('/something/else/goes/here'))
assert.strictEqual(router.match('/something/else/goes/here').handler, assertFailFunction) assert.strictEqual(router.match('/something/else/goes/here').path.handlers[0], assertFailFunction)
router.addRoute('/', assertRootFunction) router.addRoute('/', assertRootFunction)
router.compile()
assert.ok(router.match('/')) assert.ok(router.match('/'))
assert.strictEqual(router.match('/').handler, assertRootFunction) assert.strictEqual(router.match('/').path.handlers[0], assertRootFunction)
}) })
t.test('return null when no match is found', function() { t.test('return null when no match is found', function() {