UNPKG

memoirist

Version:

Elysia's Radix Tree router for fast matching dynamic parameters

308 lines (307 loc) 8.63 kB
// 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 };