2021-10-09 00:12:56 +00:00
import http from 'http'
import stream from 'stream'
2020-03-11 12:34:34 +00:00
/ * *
* Router
* /
2021-07-05 17:54:00 +00:00
class Branch {
constructor ( ) {
this . children = new Map ( )
this . paramName = null
this . fullparamName = null
this . handler = null
this . middlewares = [ ]
}
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
export const ErrorCodes = {
ERR _CONNECTION _ABORTED : 'ERR_CON_ABORTED'
}
// Taken from https://github.com/nfp-projects/koa-lite/blob/master/lib/statuses.js
const statuses = {
100 : 'Continue' , 101 : 'Switching Protocols' , 102 : 'Processing' , 103 : 'Early Hints' ,
200 : 'OK' , 201 : 'Created' , 202 : 'Accepted' , 203 : 'Non-Authoritative Information' , 204 : 'No Content' , 205 : 'Reset Content' , 206 : 'Partial Content' , 207 : 'Multi-Status' , 208 : 'Already Reported' , 226 : 'IM Used' ,
300 : 'Multiple Choices' , 301 : 'Moved Permanently' , 302 : 'Found' , 303 : 'See Other' , 304 : 'Not Modified' , 305 : 'Use Proxy' , 306 : '(Unused)' , 307 : 'Temporary Redirect' , 308 : 'Permanent Redirect' ,
400 : 'Bad Request' , 401 : 'Unauthorized' , 402 : 'Payment Required' , 403 : 'Forbidden' , 404 : 'Not Found' , 405 : 'Method Not Allowed' , 406 : 'Not Acceptable' , 407 : 'Proxy Authentication Required' , 408 : 'Request Timeout' , 409 : 'Conflict' , 410 : 'Gone' , 411 : 'Length Required' , 412 : 'Precondition Failed' , 413 : 'Payload Too Large' , 414 : 'URI Too Long' , 415 : 'Unsupported Media Type' , 416 : 'Range Not Satisfiable' , 417 : 'Expectation Failed' , 418 : 'I\'m a teapot' , 421 : 'Misdirected Request' , 422 : 'Unprocessable Entity' , 423 : 'Locked' , 424 : 'Failed Dependency' , 425 : 'Too Early' , 426 : 'Upgrade Required' , 428 : 'Precondition Required' , 429 : 'Too Many Requests' , 431 : 'Request Header Fields Too Large' , 451 : 'Unavailable For Legal Reasons' ,
500 : 'Internal Server Error' , 501 : 'Not Implemented' , 502 : 'Bad Gateway' , 503 : 'Service Unavailable' , 504 : 'Gateway Timeout' , 505 : 'HTTP Version Not Supported' , 506 : 'Variant Also Negotiates' , 507 : 'Insufficient Storage' , 508 : 'Loop Detected' , 509 : 'Bandwidth Limit Exceeded' , 510 : 'Not Extended' , 511 : 'Network Authentication Required' ,
redirect : {
300 : true ,
301 : true ,
302 : true ,
303 : true ,
305 : true ,
307 : true ,
308 : true
} ,
empty : {
204 : true ,
205 : true ,
304 : true
}
}
2020-03-11 12:34:34 +00:00
const _ _paramMapName = '__param'
2021-07-05 17:54:00 +00:00
const _ _fullParamMapName = '__fullparam'
2021-10-09 00:12:56 +00:00
function assertIsHandler ( handler , name ) {
if ( typeof ( handler ) !== 'function' ) {
throw new Error ( ` ${ name } was called with a handler that was not a function ` )
}
}
2021-07-05 17:54:00 +00:00
export class FlaskaRouter {
constructor ( ) {
this . root = new Branch ( )
}
addRoute ( route , orgMiddlewares , orgHandler ) {
if ( route [ 0 ] !== '/' )
throw new Error ( ` route " ${ route } " must start with forward slash ` )
let middlewares = orgMiddlewares
let handler = orgHandler
if ( ! orgHandler ) {
handler = orgMiddlewares
middlewares = [ ]
}
if ( middlewares && typeof ( middlewares ) === 'function' ) {
middlewares = [ middlewares ]
}
2021-10-09 00:12:56 +00:00
assertIsHandler ( handler , 'addRoute()' )
2021-07-05 17:54:00 +00:00
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 ( )
branch . children . set ( name , child )
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 ++
}
}
}
2020-03-11 12:34:34 +00:00
2021-07-05 17:54:00 +00:00
match ( orgUrl ) {
let url = orgUrl
if ( url . length > 1 && url [ url . length - 1 ] === '/' ) {
url = url . slice ( 0 , - 1 )
}
let branch = this . root
let start = 1
let end = 1
let output
let name
let char
let params = { }
if ( output = branch . children . get ( url ) ) {
return {
handler : output . handler ,
middlewares : output . middlewares ,
params : params ,
}
}
for ( let i = 1 ; i <= url . length ; i ++ ) {
char = url [ i ]
if ( ( i === url . length || char === '/' ) && end > start ) {
name = url . slice ( start , end )
if ( output = branch . children . get ( name ) ) {
branch = output
}
else if ( output = branch . children . get ( _ _paramMapName ) ) {
branch = output
params [ branch . paramName ] = name
2021-10-09 00:12:56 +00:00
}
else if ( output = branch . children . get ( _ _fullParamMapName ) ) {
params [ output . fullparamName ] = url . slice ( start )
return {
handler : output . handler ,
middlewares : output . middlewares ,
params : params ,
}
2021-07-05 17:54:00 +00:00
} else {
2021-10-09 00:12:56 +00:00
if ( output = this . root . children . get ( _ _fullParamMapName ) ) {
params = {
[ output . fullparamName ] : url . slice ( 1 )
}
return {
handler : output . handler ,
middlewares : output . middlewares ,
params : params ,
}
}
2021-07-05 17:54:00 +00:00
return null
}
i ++
end = start = i
char = url [ i ]
}
2021-10-09 00:12:56 +00:00
// 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 ) {
2021-07-05 17:54:00 +00:00
return {
handler : branch . handler ,
middlewares : branch . middlewares ,
params : params ,
}
}
if ( char === '/' ) {
end = start = i + 1
} else {
end ++
}
}
2021-10-09 00:12:56 +00:00
if ( output = this . root . children . get ( _ _fullParamMapName ) ) {
params = {
[ output . fullparamName ] : url . slice ( 1 )
}
return {
handler : output . handler ,
middlewares : output . middlewares ,
params : params ,
}
}
2021-07-05 17:54:00 +00:00
return null
}
}
2021-10-09 00:12:56 +00:00
/ * *
* Flaska
* /
export class Flaska {
constructor ( opts , orgHttp = http , orgStream = stream ) {
this . _before = [ ]
this . _beforeCompiled = null
this . _beforeAsync = [ ]
this . _beforeAsyncCompiled = null
this . _after = [ ]
this . _afterCompiled = null
this . _afterAsync = [ ]
this . _afterAsyncCompiled = null
this . _on404 = function ( ctx ) {
ctx . status = 404
ctx . body = {
status : 404 ,
message : statuses [ 404 ] ,
}
}
this . _backuperror = this . _onerror = function ( err , ctx ) {
ctx . log . error ( err )
ctx . status = 500
ctx . body = {
status : 500 ,
message : statuses [ 500 ] ,
}
}
this . _onreqerror = function ( err , ctx ) {
ctx . log . error ( err )
ctx . res . statusCode = ctx . statusCode = 400
ctx . res . end ( )
ctx . finished = true
}
this . _onreserror = function ( err , ctx ) {
ctx . log . error ( err )
}
2020-03-11 12:34:34 +00:00
2021-10-09 00:12:56 +00:00
let options = opts || { }
2021-07-05 17:54:00 +00:00
2021-10-09 00:12:56 +00:00
this . log = options . log || console
this . http = orgHttp
this . stream = orgStream
this . server = null
this . routers = {
'GET' : new FlaskaRouter ( ) ,
'POST' : new FlaskaRouter ( ) ,
'PUT' : new FlaskaRouter ( ) ,
'DELETE' : new FlaskaRouter ( ) ,
'OPTIONS' : new FlaskaRouter ( ) ,
'PATCH' : new FlaskaRouter ( ) ,
}
this . get = this . routers . GET . addRoute . bind ( this . routers . GET )
this . post = this . routers . POST . addRoute . bind ( this . routers . POST )
this . put = this . routers . PUT . addRoute . bind ( this . routers . PUT )
this . delete = this . routers . DELETE . addRoute . bind ( this . routers . DELETE )
this . options = this . routers . OPTIONS . addRoute . bind ( this . routers . OPTIONS )
this . patch = this . routers . PATCH . addRoute . bind ( this . routers . PATCH )
2021-07-05 17:54:00 +00:00
}
2021-10-09 00:12:56 +00:00
_assertIsHandler ( handler , name ) {
if ( typeof ( handler ) !== 'function' ) {
throw new Error ( ` ${ name } was called with a handler that was not a function ` )
2021-07-05 17:54:00 +00:00
}
}
2020-03-11 12:34:34 +00:00
2021-10-09 00:12:56 +00:00
devMode ( ) {
this . _backuperror = this . _onerror = function ( err , ctx ) {
ctx . log . error ( err )
ctx . status = 500
ctx . body = {
status : 500 ,
message : ` ${ statuses [ 500 ] } : ${ err . message } ` ,
stack : err . stack || '' ,
2020-03-11 12:34:34 +00:00
}
}
2021-10-09 00:12:56 +00:00
}
on404 ( handler ) {
assertIsHandler ( handler , 'on404()' )
this . _on404 = handler
}
onerror ( handler ) {
assertIsHandler ( handler , 'onerror()' )
this . _onerror = handler
}
onreqerror ( handler ) {
assertIsHandler ( handler , 'onreqerror()' )
this . _onreqerror = handler
}
onreserror ( handler ) {
assertIsHandler ( handler , 'onreserror()' )
this . _onreserror = handler
}
onerror ( handler ) {
assertIsHandler ( handler , 'onerror()' )
this . _onerror = handler
}
before ( handler ) {
assertIsHandler ( handler , 'before()' )
this . _before . push ( handler )
}
beforeAsync ( handler ) {
assertIsHandler ( handler , 'beforeAsync()' )
this . _beforeAsync . push ( handler )
}
after ( handler ) {
assertIsHandler ( handler , 'after()' )
this . _after . push ( handler )
}
afterAsync ( handler ) {
assertIsHandler ( handler , 'afterAsync()' )
this . _afterAsync . push ( handler )
}
requestStart ( req , res ) {
let url = req . url
let search = ''
let hasSearch = url . indexOf ( '?' )
if ( hasSearch > 0 ) {
search = url . slice ( hasSearch )
url = url . slice ( 0 , hasSearch )
}
let ctx = {
2021-10-10 19:57:48 +00:00
log : this . log ,
2021-10-09 00:12:56 +00:00
req : req ,
res : res ,
method : req . method ,
url : url ,
search : search ,
state : { } ,
status : 200 ,
body : null ,
type : null ,
length : null ,
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
req . on ( 'error' , ( err ) => {
this . _onreqerror ( err , ctx )
} )
res . on ( 'error' , ( err ) => {
this . _onreserror ( err , ctx )
} )
req . on ( 'aborted' , function ( ) {
ctx . aborted = true
} )
req . on ( 'close' , ( ) => {
this . requestEnded ( )
} )
try {
this . _beforeCompiled ( ctx )
if ( this . _beforeAsyncCompiled ) {
return this . _beforeAsyncCompiled ( ctx )
. then ( ( ) => {
this . requestStartInternal ( ctx )
} ) . catch ( err => {
this . requestEnd ( err , ctx )
} )
2021-07-05 17:54:00 +00:00
}
2021-10-09 00:12:56 +00:00
this . requestStartInternal ( ctx )
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
catch ( err ) {
this . requestEnd ( err , ctx )
2020-03-11 12:34:34 +00:00
}
}
2021-10-09 00:12:56 +00:00
requestStartInternal ( ctx ) {
let route = this . routers [ ctx . method ] . match ( ctx . url )
ctx . params = route . params
if ( route . middlewares . length ) {
let middle = this . handleMiddleware ( ctx , route . middlewares , 0 )
if ( middle && middle . then ) {
return middle . 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 )
} , err => {
this . requestEnd ( err , ctx )
} )
2021-07-05 17:54:00 +00:00
}
2021-10-09 00:12:56 +00:00
this . requestEnd ( null , ctx )
2021-07-05 17:54:00 +00:00
}
2021-10-09 00:12:56 +00:00
handleMiddleware ( ctx , middles , index ) {
for ( let i = index ; i < middles . length ; i ++ ) {
let res = middles [ i ] ( ctx )
if ( res && res . then ) {
return res . then ( ( ) => {
return this . handleMiddleware ( ctx , middles , i + 1 )
} )
2020-03-11 12:34:34 +00:00
}
}
2021-10-09 00:12:56 +00:00
}
requestEnd ( err , ctx ) {
if ( err ) {
this . _onerror ( err , ctx )
return ctx . res . end ( )
}
if ( ctx . req . complete ) {
return
}
ctx . res . statusCode = ctx . status
if ( statuses . empty [ ctx . status ] ) {
return ctx . res . end ( )
}
let body = ctx . body
let length = 0
if ( body && typeof ( body . pipe ) === 'function' ) {
ctx . res . setHeader ( 'Content-Type' , ctx . type || 'application/octet-stream' )
return this . stream . pipeline ( body , ctx . res , function ( ) { } )
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
if ( typeof ( body ) === 'object' ) {
body = JSON . stringify ( body )
length = Buffer . byteLength ( body )
ctx . res . setHeader ( 'Content-Type' , 'application/json; charset=utf-8' )
2021-07-05 17:54:00 +00:00
} else {
2021-10-09 00:12:56 +00:00
body = body . toString ( )
length = Buffer . byteLength ( body )
ctx . res . setHeader ( 'Content-Type' , ctx . type || 'text/plain; charset=utf-8' )
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
ctx . res . setHeader ( 'Content-Length' , length )
ctx . res . end ( body )
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
requestEnded ( ctx ) {
this . _afterCompiled ( ctx )
if ( this . _afterAsyncCompiled ) {
return this . _afterAsyncCompiled ( ctx ) . then ( )
}
}
2021-07-05 17:54:00 +00:00
2021-10-09 00:12:56 +00:00
compile ( ) {
let types = [ 'before' , 'after' ]
for ( let i = 0 ; i < types . length ; i ++ ) {
let type = types [ i ]
let args = ''
let body = ''
for ( let i = 0 ; i < this [ '_' + type ] . length ; i ++ ) {
args += ` a ${ i } , `
body += ` a ${ i } (ctx); `
}
args += 'ctx'
let func = new Function ( args , body )
this [ ` _ ${ type } Compiled ` ] = func . bind ( this , ... this [ '_' + type ] )
if ( this [ ` _ ${ type } Async ` ] . length ) {
args = ''
body = 'return Promise.all(['
for ( let i = 0 ; i < this [ ` _ ${ type } Async ` ] . length ; i ++ ) {
args += ` a ${ i } , `
body += ` a ${ i } (ctx), `
}
args += 'ctx'
body += '])'
func = new Function ( args , body )
this [ ` _ ${ type } AsyncCompiled ` ] = func . bind ( this , ... this [ ` _ ${ type } Async ` ] )
}
}
}
listen ( port , cb ) {
if ( typeof ( port ) !== 'number' ) {
throw new Error ( 'Flaska.listen() called with non-number in port' )
}
this . compile ( )
this . server = this . http . createServer ( this . requestStart . bind ( this ) )
this . server . listen ( port , cb )
}
2020-03-11 12:34:34 +00:00
}