make-fetch
Version:
Implement your own `fetch()` with node.js streams
167 lines (138 loc) • 3.96 kB
JavaScript
export const WILDCARD = '*'
const MATCH_ORDER = ['method', 'protocol', 'hostname', 'pathname']
export function makeFetch (handler, {
Request = globalThis.Request,
Response = globalThis.Response
} = {}) {
return async function fetch (...requestOptions) {
const isAlreadyRequest = requestOptions[0] instanceof Request
const request = isAlreadyRequest ? requestOptions[0] : new Request(...requestOptions)
const { body = null, ...responseOptions } = await handler(request)
const response = new Response(body, responseOptions)
return response
}
}
export function makeRoutedFetch ({
onNotFound = DEFAULT_NOT_FOUND,
onError = DEFAULT_ON_ERROR
} = {}) {
const router = new Router()
const fetch = makeFetch(async (request) => {
const route = router.route(request)
if (!route) {
return onNotFound(request)
}
try {
const response = await route.handler(request)
return response
} catch (e) {
return await onError(e, request)
}
})
return { fetch, router }
}
export function DEFAULT_NOT_FOUND () {
return { status: 404, statusText: 'Invalid URL' }
}
export function DEFAULT_ON_ERROR (e) {
return {
status: 500,
headers: {
'Content-Type': 'text/plain; charset=utf-8'
},
body: e.stack
}
}
export class Router {
constructor () {
this.routes = []
}
get (url, handler) {
return this.add('GET', url, handler)
}
head (url, handler) {
return this.add('HEAD', url, handler)
}
post (url, handler) {
return this.add('POST', url, handler)
}
put (url, handler) {
return this.add('PUT', url, handler)
}
delete (url, handler) {
return this.add('DELETE', url, handler)
}
patch (url, handler) {
return this.add('PATCH', url, handler)
}
any (url, handler) {
return this.add(WILDCARD, url, handler)
}
add (method, url, handler) {
const parsed = new URL(url)
const { hostname, protocol, pathname } = parsed
const segments = pathname.slice(1).split('/')
const route = {
protocol,
method: method.toUpperCase(),
hostname,
segments,
handler
}
this.routes.push(route)
return this
}
route (request) {
for (const route of this.routes) {
let hasFail = false
for (const property of MATCH_ORDER) {
if (!matches(request, route, property)) {
hasFail = true
}
}
if (!hasFail) {
return route
}
}
return null
}
}
function matches (request, route, property) {
if (property === 'pathname') {
const routeSegments = route.segments
const { pathname } = new URL(request.url)
const requestSegments = pathname.slice(1).split('/')
let i = 0
while (true) {
const routeLast = i === (routeSegments.length - 1)
const requestLast = i === (requestSegments.length - 1)
const routeSegment = routeSegments[i]
const requestSegment = requestSegments[i]
const routeWild = routeSegment === WILDCARD
const matches = routeWild || (routeSegment === requestSegment)
if (routeLast) {
if (routeSegment === (WILDCARD + WILDCARD)) return true
if (requestLast) return matches
return false
} else if (requestLast) {
return false
}
if (!matches) return false
i++
}
} if (property === 'hostname') {
return areEqual(route.hostname, new URL(request.url).hostname)
} else if (property === 'protocol') {
return areEqual(route.protocol, new URL(request.url).protocol)
} else if (property === 'method') {
return areEqual(route.method, request.method.toUpperCase())
} else {
const routeProperty = route[property]
const requestProperty = request[property]
return areEqual(routeProperty, requestProperty)
}
}
function areEqual (routeProperty, requestProperty) {
if (routeProperty === '*') return true
return routeProperty === requestProperty
}