memoirist
Version:
Elysia's Radix Tree router for fast matching dynamic parameters
308 lines (307 loc) • 8.63 kB
JavaScript
// src/index.ts
var createNode = (part, inert) => {
const inertMap = inert?.length ? {} : null;
if (inertMap)
for (const child of inert)
inertMap[child.part.charCodeAt(0)] = child;
return {
part,
store: null,
inert: inertMap,
params: null,
wildcardStore: null
};
};
var cloneNode = (node, part) => ({
...node,
part
});
var createParamNode = (name) => ({
name,
store: null,
inert: null
});
var Memoirist = class _Memoirist {
constructor(config = {}) {
this.config = config;
if (config.lazy)
this.find = this.lazyFind;
if (config.onParam && !Array.isArray(config.onParam))
this.config.onParam = [
this.config.onParam
];
}
root = {};
history = [];
deferred = [];
static regex = {
static: /:.+?(?=\/|$)/,
params: /:.+?(?=\/|$)/g,
optionalParams: /(\/:\w+\?)/g
};
lazyFind = (method, url) => {
if (!this.config.lazy)
return this.find;
this.build();
return this.find(method, url);
};
build() {
if (!this.config.lazy)
return;
for (const [method, path, store] of this.deferred)
this.add(method, path, store, { lazy: false, ignoreHistory: true });
this.deferred = [];
this.find = (method, url) => {
const root = this.root[method];
if (!root)
return null;
return matchRoute(
url,
url.length,
root,
0,
this.config.onParam
);
};
}
add(method, path, store, {
ignoreError = false,
ignoreHistory = false,
lazy = this.config.lazy
} = {}) {
if (lazy) {
this.find = this.lazyFind;
this.deferred.push([method, path, store]);
return store;
}
if (typeof path !== "string")
throw new TypeError("Route path must be a string");
if (path === "")
path = "/";
else if (path[0] !== "/")
path = `/${path}`;
const isWildcard = path[path.length - 1] === "*";
const optionalParams = path.match(_Memoirist.regex.optionalParams);
if (optionalParams) {
const originalPath = path.replaceAll("?", "");
this.add(method, originalPath, store, {
ignoreError,
ignoreHistory,
lazy
});
for (let i = 0; i < optionalParams.length; i++) {
let newPath = path.replace(optionalParams[i], "");
this.add(method, newPath, store, {
ignoreError: true,
ignoreHistory,
lazy
});
}
return store;
}
if (optionalParams)
path = path.replaceAll("?", "");
if (this.history.find(([m, p, s]) => m === method && p === path))
return store;
if (isWildcard || optionalParams && path.charCodeAt(path.length - 1) === 63)
path = path.slice(0, -1);
if (!ignoreHistory)
this.history.push([method, path, store]);
const inertParts = path.split(_Memoirist.regex.static);
const paramParts = path.match(_Memoirist.regex.params) || [];
if (inertParts[inertParts.length - 1] === "")
inertParts.pop();
let node;
if (!this.root[method])
node = this.root[method] = createNode("/");
else
node = this.root[method];
let paramPartsIndex = 0;
for (let i = 0; i < inertParts.length; ++i) {
let part = inertParts[i];
if (i > 0) {
const param = paramParts[paramPartsIndex++].slice(1);
if (node.params === null)
node.params = createParamNode(param);
else if (node.params.name !== param) {
if (ignoreError)
return store;
else
throw new Error(
`Cannot create route "${path}" with parameter "${param}" because a route already exists with a different parameter name ("${node.params.name}") in the same location`
);
}
const params = node.params;
if (params.inert === null) {
node = params.inert = createNode(part);
continue;
}
node = params.inert;
}
for (let j = 0; ; ) {
if (j === part.length) {
if (j < node.part.length) {
const childNode = cloneNode(node, node.part.slice(j));
Object.assign(node, createNode(part, [childNode]));
}
break;
}
if (j === node.part.length) {
if (node.inert === null)
node.inert = {};
const inert = node.inert[part.charCodeAt(j)];
if (inert) {
node = inert;
part = part.slice(j);
j = 0;
continue;
}
const childNode = createNode(part.slice(j));
node.inert[part.charCodeAt(j)] = childNode;
node = childNode;
break;
}
if (part[j] !== node.part[j]) {
const existingChild = cloneNode(node, node.part.slice(j));
const newChild = createNode(part.slice(j));
Object.assign(
node,
createNode(node.part.slice(0, j), [
existingChild,
newChild
])
);
node = newChild;
break;
}
++j;
}
}
if (paramPartsIndex < paramParts.length) {
const param = paramParts[paramPartsIndex];
const name = param.slice(1);
if (node.params === null)
node.params = createParamNode(name);
else if (node.params.name !== name) {
if (ignoreError)
return store;
else
throw new Error(
`Cannot create route "${path}" with parameter "${name}" because a route already exists with a different parameter name ("${node.params.name}") in the same location`
);
}
if (node.params.store === null)
node.params.store = store;
return node.params.store;
}
if (isWildcard) {
if (node.wildcardStore === null)
node.wildcardStore = store;
return node.wildcardStore;
}
if (node.store === null)
node.store = store;
return node.store;
}
find(method, url) {
const root = this.root[method];
if (!root)
return null;
return matchRoute(
url,
url.length,
root,
0,
this.config.onParam
);
}
};
var matchRoute = (url, urlLength, node, startIndex, onParam) => {
const part = node.part;
const length = part.length;
const endIndex = startIndex + length;
if (length > 1) {
if (endIndex > urlLength)
return null;
if (length < 15) {
for (let i = 1, j = startIndex + 1; i < length; ++i, ++j)
if (part.charCodeAt(i) !== url.charCodeAt(j))
return null;
} else if (url.slice(startIndex, endIndex) !== part)
return null;
}
if (endIndex === urlLength) {
if (node.store !== null)
return {
store: node.store,
params: {}
};
if (node.wildcardStore !== null)
return {
store: node.wildcardStore,
params: { "*": "" }
};
return null;
}
if (node.inert !== null) {
const inert = node.inert[url.charCodeAt(endIndex)];
if (inert !== void 0) {
const route = matchRoute(url, urlLength, inert, endIndex, onParam);
if (route !== null)
return route;
}
}
if (node.params !== null) {
const { store, name, inert } = node.params;
const slashIndex = url.indexOf("/", endIndex);
if (slashIndex !== endIndex) {
if (slashIndex === -1 || slashIndex >= urlLength) {
if (store !== null) {
const params = {};
params[name] = url.substring(endIndex, urlLength);
if (onParam)
for (let i = 0; i < onParam.length; i++) {
let temp = onParam[i](params[name], name);
if (temp !== void 0)
params[name] = temp;
}
return {
store,
params
};
}
} else if (inert !== null) {
const route = matchRoute(
url,
urlLength,
inert,
slashIndex,
onParam
);
if (route !== null) {
route.params[name] = url.substring(endIndex, slashIndex);
if (onParam)
for (let i = 0; i < onParam.length; i++) {
let temp = onParam[i](route.params[name], name);
if (temp !== void 0)
route.params[name] = temp;
}
return route;
}
}
}
}
if (node.wildcardStore !== null)
return {
store: node.wildcardStore,
params: {
"*": url.substring(endIndex, urlLength)
}
};
return null;
};
var src_default = Memoirist;
export {
Memoirist,
src_default as default
};