UNPKG

@iopa/router

Version:

Lightweight and fast router for IOPA applications

331 lines (325 loc) 9.62 kB
// src/index.ts import { TraceEvent as TraceEvent2, util as util2 } from "iopa"; // src/compose.ts import { TraceEvent } from "iopa"; function compose(middleware) { return async (context) => { let index = -1; return dispatch(0); async function dispatch(i) { if (i <= index) { return Promise.reject(new Error("next() called multiple times")); } const handler = middleware[i]; index = i; if (i > 0) { const prev = middleware[i - 1]; context.dispatchEvent(new TraceEvent("trace-next", { label: prev.id })); } if (!handler) { if (i > 0) { const prev = middleware[i - 1]; context.dispatchEvent(new TraceEvent("trace-next-resume", { label: prev.id })); } return; } context.dispatchEvent(new TraceEvent("trace-start", { label: handler.id })); return Promise.resolve(handler(context, dispatch.bind(null, i + 1))).then(async (res) => { context.respondWith(res); context.dispatchEvent(new TraceEvent("trace-end", { label: handler.id })); if (i > 0) { const prev = middleware[i - 1]; context.dispatchEvent(new TraceEvent("trace-next-resume", { label: prev.id })); } }); } }; } // src/constants.ts var METHOD_NAME_ALL = "ALL"; var METHOD_NAME_ALL_LOWERCASE = "all"; var METHODS = [ "get", "post", "put", "delete", "head", "options", "patch" ]; // src/trie-router/node.ts import { util } from "iopa"; var { url: { splitPath, getPattern } } = util; function findParam(node, name) { for (let i = 0, len = node.patterns.length; i < len; i++) { if (typeof node.patterns[i] === "object" && node.patterns[i][1] === name) { return true; } } const nodes = Object.values(node.children); for (let i = 0, len = nodes.length; i < len; i++) { if (findParam(nodes[i], name)) { return true; } } return false; } var Node = class { constructor(method, handler, children) { this.order = 0; this.children = children || {}; this.methods = []; if (method && handler) { const m = {}; m[method] = { handler, score: 0, name: this.name }; this.methods = [m]; } this.patterns = []; } insert(method, path, handler) { this.name = `${method} ${path}`; this.order = ++this.order; let curNode = this; const parts = splitPath(path); const parentPatterns = []; const errorMessage = (name) => { return `Duplicate param name, use another name instead of '${name}' - ${method} ${path} <--- '${name}'`; }; for (let i = 0, len = parts.length; i < len; i++) { const p = parts[i]; if (Object.keys(curNode.children).includes(p)) { parentPatterns.push(...curNode.patterns); curNode = curNode.children[p]; continue; } curNode.children[p] = new Node(); const pattern = getPattern(p); if (pattern) { if (typeof pattern === "object") { for (let j = 0, len2 = parentPatterns.length; j < len2; j++) { if (typeof parentPatterns[j] === "object" && parentPatterns[j][1] === pattern[1]) { throw new Error(errorMessage(pattern[1])); } } if (Object.values(curNode.children).some((n) => findParam(n, pattern[1]))) { throw new Error(errorMessage(pattern[1])); } } curNode.patterns.push(pattern); parentPatterns.push(...curNode.patterns); } parentPatterns.push(...curNode.patterns); curNode = curNode.children[p]; } let score = 1; if (path === "*") { score = score + this.order * 0.01; } else { score = parts.length + this.order * 0.01; } if (!curNode.methods.length) { curNode.methods = []; } const m = {}; const handlerSet = { handler, name: this.name, score }; m[method] = handlerSet; curNode.methods.push(m); return curNode; } _getHandlerSets(node, method, wildcard) { const handlerSets = []; node.methods.map((m) => { const handlerSet = m[method] || m[METHOD_NAME_ALL]; if (handlerSet !== void 0) { const hs = { ...handlerSet }; if (wildcard) { hs.score = handlerSet.score - 1; } handlerSets.push(hs); return; } }); return handlerSets; } _next(node, part, method, isLast) { const handlerSets = []; const nextNodes = []; const params = {}; for (let j = 0, len = node.patterns.length; j < len; j++) { const pattern = node.patterns[j]; if (pattern === "*") { const astNode = node.children["*"]; if (astNode) { handlerSets.push(...this._getHandlerSets(astNode, method)); nextNodes.push(astNode); } } if (part === "") continue; const [key, name, matcher] = pattern; if (matcher === true || matcher instanceof RegExp && matcher.test(part)) { if (typeof key === "string") { if (isLast === true) { handlerSets.push(...this._getHandlerSets(node.children[key], method)); } nextNodes.push(node.children[key]); } if (typeof name === "string") { params[name] = part; } } } const nextNode = node.children[part]; if (nextNode) { if (isLast === true) { if (nextNode.children["*"]) { handlerSets.push(...this._getHandlerSets(nextNode.children["*"], method, true)); } handlerSets.push(...this._getHandlerSets(nextNode, method)); } nextNodes.push(nextNode); } const next = { nodes: nextNodes, handlerSets, params }; return next; } search(method, path) { const handlerSets = []; let params = {}; const curNode = this; let curNodes = [curNode]; const parts = splitPath(path); for (let i = 0, len = parts.length; i < len; i++) { const p = parts[i]; const isLast = i === len - 1; const tempNodes = []; for (let j = 0, len2 = curNodes.length; j < len2; j++) { const res = this._next(curNodes[j], p, method, isLast); if (res.nodes.length === 0) { continue; } handlerSets.push(...res.handlerSets); params = Object.assign(params, res.params); tempNodes.push(...res.nodes); } curNodes = tempNodes; } if (handlerSets.length <= 0) return null; const handlers = handlerSets.sort((a, b) => { return a.score - b.score; }).map((s) => { return s.handler; }); return { handlers, params }; } }; // src/trie-router/router.ts var TrieRouter = class { constructor() { this.node = new Node(); } add(method, path, handler) { this.node.insert(method, path, handler); } match(method, path) { return this.node.search(method, path); } }; // src/index.ts var { url: { mergePath, getPathFromURL } } = util2; var RouterMiddleware = class { constructor(app, options = {}) { this.router = new TrieRouter(); this.strict = true; this._basePath = ""; this.routes = []; const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE]; allMethods.forEach((method) => { app[method] = (args1, args2, args3) => { let _path = "/"; if (typeof args1 === "string") { _path = args1; } else { throw new Error("first argument must be a string path"); } if (typeof args2 !== "string") { this._addRoute(method, _path, args2, args3); } else { throw new Error("second argument must be an app func"); } return app; }; }); Object.assign(this, options); } async invoke(context, next) { const result = await this._matchRoute(context); if (!result) { return next(); } this._preprocessRoute(result, context); try { await this._validateRoute(result, context); await this._handleRoute(result, context, next); } catch (ex) { console.log(ex); context.respondWith(ex); return; } } _addRoute(method, path, handler, options) { method = method.toUpperCase(); if (this._basePath) { path = mergePath(this._basePath, path); } this.router.add(method, path, handler); handler.id = handler.id || `${method} ${path} Handler #${this.routes.filter((r2) => r2.handler.id.startsWith(`${method} ${path} Handler`)).length + 1}`; handler.options = options; const r = { path, method, handler }; this.routes.push(r); console.log("ADDED", r); return r; } async _matchRoute(context) { const path = getPathFromURL(context.get("iopa.OriginalUrl"), { strict: this.strict }); const method = context.get("iopa.Method"); return this.router.match(method, path); } _preprocessRoute(result, context) { const params = result ? result.params : {}; context.set("iopa.Params", params); } async _validateRoute(result, context) { } async _handleRoute(result, context, next) { const handlers = result ? result.handlers : []; const composed = compose(handlers); context.dispatchEvent(new TraceEvent2("trace-next", { label: context.get("server.CurrentMiddleware") })); await composed(context); context.dispatchEvent(new TraceEvent2("trace-next-resume", { label: context.get("server.CurrentMiddleware") })); if (!context.response.get("iopa.IsFinalized")) { return next(); } } }; export { RouterMiddleware as default };