@iopa/router
Version:
Lightweight and fast router for IOPA applications
331 lines (325 loc) • 9.62 kB
JavaScript
// 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
};