+19
| // src/providers/bun.ts | ||
| var bunProviderFactory = ({ h11 }) => { | ||
| return async (req, server) => { | ||
| const res = await h11.exec({ | ||
| req, | ||
| providers: { | ||
| bun: { | ||
| req, | ||
| server | ||
| } | ||
| }, | ||
| data: {} | ||
| }); | ||
| return res; | ||
| }; | ||
| }; | ||
| export { | ||
| bunProviderFactory | ||
| }; |
+187
| // src/radix/node/node.ts | ||
| class Node { | ||
| segment; | ||
| handlers = {}; | ||
| children = []; | ||
| wildParent = false; | ||
| parent; | ||
| constructor({ segment, parent }) { | ||
| this.segment = segment; | ||
| this.parent = parent; | ||
| } | ||
| } | ||
| var addNodeToChildren = (parent, node) => { | ||
| const children = parent.children; | ||
| if (children.length === 0) { | ||
| children.push(node); | ||
| return; | ||
| } | ||
| if (node.segment === "**") { | ||
| children.push(node); | ||
| return; | ||
| } else if (node.segment[0] === ":") { | ||
| for (let i = children.length - 1;i >= 0; i--) { | ||
| const compareNode = children[i]; | ||
| if (compareNode.segment === "**") { | ||
| continue; | ||
| } | ||
| children.splice(i + 1, 0, node); | ||
| return; | ||
| } | ||
| children.unshift(node); | ||
| } else { | ||
| for (let i = children.length - 1;i >= 0; i--) { | ||
| const compareNode = children[i]; | ||
| if (compareNode.segment === "**" || compareNode.segment[0] === ":") { | ||
| continue; | ||
| } | ||
| children.splice(i + 1, 0, node); | ||
| return; | ||
| } | ||
| children.unshift(node); | ||
| } | ||
| }; | ||
| // src/radix/radix.ts | ||
| class Radix { | ||
| root = new Node({ segment: "" }); | ||
| find(path, method = "GET") { | ||
| const segments = path.split("/"); | ||
| const params = {}; | ||
| let currentNode = this.root; | ||
| let lastWild = null; | ||
| outer: | ||
| for (let i = 0;i < segments.length; i++) { | ||
| const segment = segments[i]; | ||
| if (currentNode.wildParent === true) { | ||
| const wild = currentNode.children[currentNode.children.length - 1]; | ||
| const wildHandlerContainer = wild.handlers[method]; | ||
| if (wildHandlerContainer) { | ||
| lastWild = { | ||
| node: wild, | ||
| handler: wildHandlerContainer, | ||
| params: { ...params, wild: segments.slice(i).join("/") } | ||
| }; | ||
| } | ||
| } | ||
| if (currentNode.segment[0] === ":") { | ||
| params[currentNode.segment.slice(1)] = segment; | ||
| } | ||
| const isLastSegment = i + 1 === segments.length; | ||
| if (isLastSegment) { | ||
| break; | ||
| } | ||
| if (currentNode.children.length === 0) { | ||
| return lastWild; | ||
| } | ||
| for (const child of currentNode.children) { | ||
| const nextSegment = segments[i + 1]; | ||
| if (child.segment === nextSegment) { | ||
| currentNode = child; | ||
| continue outer; | ||
| } | ||
| if (child.segment[0] === ":" && nextSegment !== "") { | ||
| currentNode = child; | ||
| continue outer; | ||
| } | ||
| } | ||
| return lastWild; | ||
| } | ||
| const handlerContainer = currentNode.handlers[method]; | ||
| if (!handlerContainer) { | ||
| return lastWild; | ||
| } | ||
| return { | ||
| node: currentNode, | ||
| params, | ||
| handler: handlerContainer | ||
| }; | ||
| } | ||
| add(pattern, method = "GET", handler) { | ||
| const patternSegments = pattern.slice(1).split("/"); | ||
| let currentNode = this.root; | ||
| outer: | ||
| for (let i = 0;i < patternSegments.length; i++) { | ||
| const segment = patternSegments[i]; | ||
| for (const child of currentNode.children) { | ||
| if (child.segment === segment) { | ||
| currentNode = child; | ||
| continue outer; | ||
| } | ||
| } | ||
| const newNode = new Node({ segment, parent: currentNode }); | ||
| addNodeToChildren(currentNode, newNode); | ||
| if (segment === "**") { | ||
| currentNode.wildParent = true; | ||
| } | ||
| currentNode = newNode; | ||
| } | ||
| currentNode.handlers[method] = handler; | ||
| return currentNode; | ||
| } | ||
| } | ||
| // src/h11.ts | ||
| var FORCE_STOP = Symbol("FORCE_STOP"); | ||
| var defaultOnNotFound = (req) => { | ||
| console.log(`h9: Not found ${req.url}`); | ||
| return new Response("Not Found", { | ||
| status: 404, | ||
| statusText: "Not Found 404", | ||
| headers: { | ||
| "Content-Type": "text/plain" | ||
| } | ||
| }); | ||
| }; | ||
| var defaultOnError = ({ req, error }) => { | ||
| console.error(`h9: Error ${req.url} - ${error.message}`); | ||
| return new Response("Error 500", { | ||
| status: 500, | ||
| statusText: "System error 500", | ||
| headers: { | ||
| "Content-Type": "text/plain" | ||
| } | ||
| }); | ||
| }; | ||
| class H11 { | ||
| types; | ||
| radix = new Radix; | ||
| onNotFound = defaultOnNotFound; | ||
| onError = defaultOnError; | ||
| addRoute(pattern, method, handler) { | ||
| const preparedHandler = typeof handler === "function" ? { handler, plugins: [] } : handler; | ||
| this.radix.add(pattern, method, preparedHandler); | ||
| } | ||
| get(pattern, handler) { | ||
| this.addRoute(pattern, "GET", handler); | ||
| return this; | ||
| } | ||
| post(pattern, handler) { | ||
| this.addRoute(pattern, "POST", handler); | ||
| return this; | ||
| } | ||
| async exec({ req, data, providers }) { | ||
| const url = new URL(req.url); | ||
| const findResult = this.radix.find(url.pathname, req.method); | ||
| if (!findResult) { | ||
| return this.onNotFound(req); | ||
| } | ||
| const props = { req, params: findResult.params, data, providers }; | ||
| try { | ||
| for (const plugin of findResult.handler.plugins) { | ||
| const response2 = await plugin(props); | ||
| if (response2) { | ||
| return response2; | ||
| } | ||
| } | ||
| const response = await findResult.handler.handler(props); | ||
| return response; | ||
| } catch (error) { | ||
| return this.onError({ ...props, error }); | ||
| } | ||
| } | ||
| } | ||
| export { | ||
| H11 | ||
| }; |
+76
| // src/providers/node.ts | ||
| import {Readable} from "node:stream"; | ||
| var joinPaths = (elem1, elem2) => { | ||
| if (elem1.at(-1) === "/") { | ||
| if (elem2[0] === "/") { | ||
| return `${elem1}${elem2.slice(1)}`; | ||
| } else { | ||
| return elem1 + elem2; | ||
| } | ||
| } else { | ||
| if (elem2[0] === "/") { | ||
| return elem1 + elem2; | ||
| } else { | ||
| return `${elem1}/${elem2}`; | ||
| } | ||
| } | ||
| }; | ||
| var getAddress = (server) => { | ||
| const address = server.address(); | ||
| if (typeof address !== "object" || address === null) { | ||
| return; | ||
| } | ||
| const protocol = "http"; | ||
| const port = `:${address.port}`; | ||
| let url; | ||
| if (address.family === "IPv6") { | ||
| url = `[${address.address}]`; | ||
| } else { | ||
| url = address.address; | ||
| } | ||
| return `${protocol}://${url}${port}`; | ||
| }; | ||
| var createReqFromNode = (req, origin) => { | ||
| const method = req.method; | ||
| const headers = new Headers(req.headers); | ||
| const body = method === "GET" || method === "HEAD" ? null : Readable.toWeb(req); | ||
| return new Request(joinPaths(origin, req.url), { | ||
| method, | ||
| headers, | ||
| body | ||
| }); | ||
| }; | ||
| var sendNodeRes = (responseNative, res) => { | ||
| const headers = {}; | ||
| responseNative.headers.forEach((value, key) => headers[key] = value); | ||
| res.writeHead(responseNative.status, responseNative.statusText, headers); | ||
| if (responseNative.body) { | ||
| const resStream = Readable.fromWeb(responseNative.body); | ||
| resStream.pipe(res); | ||
| } else { | ||
| res.end(); | ||
| } | ||
| }; | ||
| var nodeProviderFactory = ({ h11 }) => async ({ | ||
| req, | ||
| res, | ||
| origin | ||
| }) => { | ||
| const preapredOrigin = req.headers["host"] || origin; | ||
| const preparedRes = createReqFromNode(req, preapredOrigin); | ||
| const execRes = await h11.exec({ | ||
| req: preparedRes, | ||
| data: {}, | ||
| providers: { | ||
| node: { | ||
| req, | ||
| res | ||
| } | ||
| } | ||
| }); | ||
| sendNodeRes(execRes, res); | ||
| }; | ||
| export { | ||
| nodeProviderFactory, | ||
| getAddress | ||
| }; |
| import { Radix } from './radix/radix.ts'; | ||
| export type Method = 'GET' | 'POST' | 'DELET' | 'PUT' | 'PATCH'; | ||
| type HandlerResponse = Promise<Response> | Response; | ||
| export type ExecProps = { | ||
| req: Request; | ||
| providers: Record<string, any>; | ||
| data: Record<any, any>; | ||
| }; | ||
| export type HandlerPure = (props: Props) => HandlerResponse; | ||
| export type HandlerContainer = { | ||
| handler: HandlerPure; | ||
| plugins: Plugin[]; | ||
| }; | ||
| type Props = { | ||
| req: Request; | ||
| params: Record<string, string>; | ||
| data: Record<any, any>; | ||
| }; | ||
| type Handler = HandlerPure | HandlerContainer; | ||
| export type Plugin = (props: Props) => HandlerResponse; | ||
| type NotFoundHandler = (req: Request) => HandlerResponse; | ||
| type ErrorHandler = (props: { | ||
| error: Error; | ||
| } & Props) => HandlerResponse; | ||
| export declare class H11<TExecProps extends ExecProps = ExecProps> { | ||
| types: TExecProps; | ||
| radix: Radix; | ||
| onNotFound: NotFoundHandler; | ||
| onError: ErrorHandler; | ||
| private addRoute; | ||
| get(pattern: string, handler: Handler): this; | ||
| post(pattern: string, handler: Handler): this; | ||
| exec({ req, data, providers }: TExecProps): Promise<Response>; | ||
| } | ||
| export {}; |
| import type { Server } from 'bun'; | ||
| import type { H11 } from '../h11.ts'; | ||
| export declare const bunProviderFactory: ({ h11 }: { | ||
| h11: H11; | ||
| }) => (req: Request, server: Server) => Promise<Response>; |
| import type { IncomingMessage, ServerResponse, Server } from 'node:http'; | ||
| import type { H11 } from '../h11.ts'; | ||
| export declare const getAddress: (server: Server) => string | undefined; | ||
| export declare const nodeProviderFactory: ({ h11 }: { | ||
| h11: H11; | ||
| }) => ({ req, res, origin, }: { | ||
| req: IncomingMessage; | ||
| res: ServerResponse<IncomingMessage>; | ||
| origin: string; | ||
| }) => Promise<void>; |
| import type { HandlerContainer, Method } from '../../h11.ts'; | ||
| export declare class Node { | ||
| segment: string; | ||
| handlers: Partial<Record<Method, HandlerContainer>>; | ||
| children: Node[]; | ||
| wildParent: boolean; | ||
| parent?: Node; | ||
| constructor({ segment, parent }: { | ||
| segment: string; | ||
| parent?: Node; | ||
| }); | ||
| } | ||
| export declare const addNodeToChildren: (parent: Node, node: Node) => void; |
| import type { HandlerContainer, Method } from '../h11.ts'; | ||
| import { Node } from './node/node.ts'; | ||
| type Params = Record<string, string>; | ||
| export declare class Radix { | ||
| root: Node; | ||
| find(path: string, method?: Method): { | ||
| handler: HandlerContainer; | ||
| params: Params; | ||
| node: Node; | ||
| } | null; | ||
| add(pattern: string, method: Method | undefined, handler: HandlerContainer): Node; | ||
| } | ||
| export {}; |
+5
-4
| { | ||
| "name": "h11", | ||
| "type": "module", | ||
| "version": "0.0.2", | ||
| "version": "0.0.3", | ||
| "scripts": { | ||
| "build": "bun run ./build.ts" | ||
| }, | ||
| "files": [ | ||
| "./dist/**" | ||
| ], | ||
| "exports": { | ||
@@ -24,5 +27,3 @@ ".": { | ||
| "@types/bun": "latest", | ||
| "typescript": "^5.0.0" | ||
| }, | ||
| "dependencies": { | ||
| "typescript": "^5.0.0", | ||
| "mitata": "^0.1.14", | ||
@@ -29,0 +30,0 @@ "utftu": "^1.0.26" |
| { | ||
| "singleQuote": true | ||
| } |
-22
| import { $ } from 'bun'; | ||
| await $`rm -rf ./dist`; | ||
| await Promise.all([ | ||
| Bun.build({ | ||
| entrypoints: ['./src/h11.ts'], | ||
| outdir: './dist', | ||
| }), | ||
| $`tsc --project tsconfig.types.json`, | ||
| Bun.build({ | ||
| entrypoints: ['./src/providers/bun.ts'], | ||
| outdir: './dist', | ||
| }), | ||
| $`tsc --project tsconfig.types.bun.json`, | ||
| Bun.build({ | ||
| entrypoints: ['./src/providers/node.ts'], | ||
| outdir: './dist', | ||
| external: ['*'], | ||
| }), | ||
| $`tsc --project tsconfig.types.node.json`, | ||
| ]); |
Sorry, the diff of this file is not supported yet
| import { H11 } from '../src/h11.ts'; | ||
| import { bunProviderFactory } from '../src/providers/bun.ts'; | ||
| const h11 = new H11(); | ||
| h11.get('/hello/world/sasha', () => { | ||
| return new Response(`it is a super path`); | ||
| }); | ||
| h11.get('/hello/world/:name', ({ params }) => { | ||
| return new Response(`hello world mister ${params.name}`); | ||
| }); | ||
| h11.get('/hello/**', ({ params }) => { | ||
| return new Response(`it is a wild path ${params.wild}`); | ||
| }); | ||
| const bunProvider = bunProviderFactory({ h11 }); | ||
| // const server = Bun.serve({ | ||
| // // hostname: 'hello.world', | ||
| // unix: '\0benchmark-h11-bun', | ||
| // // port: '443', | ||
| // async fetch(req, server) { | ||
| // console.log('-----', 'req.url', req.url); | ||
| // // console.log('-----', 'args', args); | ||
| // // req.body. | ||
| // const res = await bunProvider(req, server); | ||
| // return res; | ||
| // }, | ||
| // }); | ||
| // console.log('-----', 'server', server.url); | ||
| Bun.serve({ | ||
| unix: '/tmp/my-socket.sock', // abstract namespace socket | ||
| fetch(req) { | ||
| return new Response(`404!`); | ||
| }, | ||
| }); |
| import { createServer } from 'node:http'; | ||
| import { H11 } from '../src/h11.ts'; | ||
| import { | ||
| getAddress, | ||
| nodeProviderFactory, | ||
| } from '../src/providers/nodejs-old.ts'; | ||
| const h11 = new H11(); | ||
| h11.get('/hello/world/sasha', () => { | ||
| return new Response(`it is a super path`); | ||
| }); | ||
| h11.get('/hello/world/:name', ({ params }) => { | ||
| return new Response(`hello world mister ${params.name}`); | ||
| }); | ||
| h11.get('/hello/**', ({ params }) => { | ||
| return new Response(`it is a wild path ${params.wild}`); | ||
| }); | ||
| h11.get('/**', ({ req }) => { | ||
| return new Response(req.url); | ||
| }); | ||
| const nodeProvider = nodeProviderFactory({ h11 }); | ||
| const server = createServer((req, res) => { | ||
| nodeProvider({ | ||
| req, | ||
| res, | ||
| origin: getAddress(server) || 'http://localhost:3000', | ||
| }); | ||
| }); | ||
| server.listen(3000); |
| import { Readable } from 'stream'; | ||
| console.log('-----', 'Readable', Readable); |
-102
| import { Radix } from './radix/radix.ts'; | ||
| const FORCE_STOP = Symbol('FORCE_STOP'); | ||
| export type Method = 'GET' | 'POST' | 'DELET' | 'PUT' | 'PATCH'; | ||
| type HandlerResponse = Promise<Response> | Response; | ||
| export type ExecProps = { | ||
| req: Request; | ||
| providers: Record<string, any>; | ||
| data: Record<any, any>; | ||
| }; | ||
| export type HandlerPure = (props: Props) => HandlerResponse; | ||
| export type HandlerContainer = { | ||
| handler: HandlerPure; | ||
| plugins: Plugin[]; | ||
| }; | ||
| type Props = { | ||
| req: Request; | ||
| params: Record<string, string>; | ||
| data: Record<any, any>; | ||
| }; | ||
| type Handler = HandlerPure | HandlerContainer; | ||
| export type Plugin = (props: Props) => HandlerResponse; | ||
| type NotFoundHandler = (req: Request) => HandlerResponse; | ||
| type ErrorHandler = (props: { error: Error } & Props) => HandlerResponse; | ||
| const defaultOnNotFound: NotFoundHandler = (req) => { | ||
| console.log(`h9: Not found ${req.url}`); | ||
| return new Response('Not Found', { | ||
| status: 404, | ||
| statusText: 'Not Found 404', | ||
| headers: { | ||
| 'Content-Type': 'text/plain', | ||
| }, | ||
| }); | ||
| }; | ||
| const defaultOnError: ErrorHandler = ({ req, error }) => { | ||
| console.error(`h9: Error ${req.url} - ${error.message}`); | ||
| return new Response('Error 500', { | ||
| status: 500, | ||
| statusText: 'System error 500', | ||
| headers: { | ||
| 'Content-Type': 'text/plain', | ||
| }, | ||
| }); | ||
| }; | ||
| export class H11<TExecProps extends ExecProps = ExecProps> { | ||
| types!: TExecProps; | ||
| radix = new Radix(); | ||
| onNotFound: NotFoundHandler = defaultOnNotFound; | ||
| onError: ErrorHandler = defaultOnError; | ||
| private addRoute(pattern: string, method: Method, handler: Handler) { | ||
| const preparedHandler = | ||
| typeof handler === 'function' ? { handler, plugins: [] } : handler; | ||
| this.radix.add(pattern, method, preparedHandler); | ||
| } | ||
| get(pattern: string, handler: Handler) { | ||
| this.addRoute(pattern, 'GET', handler); | ||
| return this; | ||
| } | ||
| post(pattern: string, handler: Handler) { | ||
| this.addRoute(pattern, 'POST', handler); | ||
| return this; | ||
| } | ||
| async exec({ req, data, providers }: TExecProps) { | ||
| const url = new URL(req.url); | ||
| const findResult = this.radix.find(url.pathname, req.method as any); | ||
| if (!findResult) { | ||
| return this.onNotFound(req); | ||
| } | ||
| const props = { req, params: findResult.params, data, providers }; | ||
| try { | ||
| for (const plugin of findResult.handler.plugins) { | ||
| const response = await plugin(props); | ||
| if (response) { | ||
| return response; | ||
| } | ||
| } | ||
| const response = await findResult.handler.handler(props); | ||
| return response; | ||
| } catch (error) { | ||
| return this.onError({ ...props, error: error as Error }); | ||
| } | ||
| } | ||
| } |
| import type { Server } from 'bun'; | ||
| import type { H11 } from '../h11.ts'; | ||
| export const bunProviderFactory = ({ h11 }: { h11: H11 }) => { | ||
| return async (req: Request, server: Server) => { | ||
| const res = await h11.exec({ | ||
| req, | ||
| providers: { | ||
| bun: { | ||
| req, | ||
| server, | ||
| }, | ||
| }, | ||
| data: {}, | ||
| }); | ||
| return res; | ||
| }; | ||
| }; |
| import type { IncomingMessage, ServerResponse, Server } from 'node:http'; | ||
| import { Readable } from 'node:stream'; | ||
| import type { H11 } from '../h11.ts'; | ||
| const joinPaths = (elem1: string, elem2: string) => { | ||
| if (elem1.at(-1) === '/') { | ||
| if (elem2[0] === '/') { | ||
| return `${elem1}${elem2.slice(1)}`; | ||
| } else { | ||
| return elem1 + elem2; | ||
| } | ||
| } else { | ||
| if (elem2[0] === '/') { | ||
| return elem1 + elem2; | ||
| } else { | ||
| return `${elem1}/${elem2}`; | ||
| } | ||
| } | ||
| }; | ||
| export const getAddress = (server: Server) => { | ||
| const address = server.address(); | ||
| if (typeof address !== 'object' || address === null) { | ||
| return; | ||
| } | ||
| const protocol = 'http'; | ||
| const port = `:${address.port}`; | ||
| let url: string; | ||
| if (address.family === 'IPv6') { | ||
| url = `[${address.address}]`; | ||
| } else { | ||
| url = address.address; | ||
| } | ||
| return `${protocol}://${url}${port}`; | ||
| }; | ||
| const createReqFromNode = (req: IncomingMessage, origin: string) => { | ||
| const method = req.method; | ||
| const headers = new Headers(req.headers as Record<string, string>); | ||
| const body = | ||
| method === 'GET' || method === 'HEAD' | ||
| ? null | ||
| : (Readable.toWeb(req) as any as ReadableStream); | ||
| // Создаем и возвращаем новый Request объект | ||
| return new Request(joinPaths(origin, req.url!), { | ||
| method, | ||
| headers, | ||
| body, | ||
| }); | ||
| }; | ||
| const sendNodeRes = ( | ||
| responseNative: Response, | ||
| res: ServerResponse<IncomingMessage> | ||
| ) => { | ||
| const headers: Record<string, string> = {}; | ||
| responseNative.headers.forEach((value, key) => (headers[key] = value)); | ||
| res.writeHead(responseNative.status, responseNative.statusText, headers); | ||
| if (responseNative.body) { | ||
| const resStream = Readable.fromWeb(responseNative.body as any); | ||
| resStream.pipe(res); | ||
| } else { | ||
| res.end(); | ||
| } | ||
| }; | ||
| export const nodeProviderFactory = | ||
| ({ h11 }: { h11: H11 }) => | ||
| async ({ | ||
| req, | ||
| res, | ||
| origin, | ||
| }: { | ||
| req: IncomingMessage; | ||
| res: ServerResponse<IncomingMessage>; | ||
| origin: string; | ||
| }) => { | ||
| const preapredOrigin = req.headers['host'] || origin; | ||
| const preparedRes = createReqFromNode(req, preapredOrigin); | ||
| const execRes = await h11.exec({ | ||
| req: preparedRes, | ||
| data: {}, | ||
| providers: { | ||
| node: { | ||
| req, | ||
| res, | ||
| }, | ||
| }, | ||
| }); | ||
| sendNodeRes(execRes, res); | ||
| }; |
| export type ProviderResponse = { | ||
| req: Request; | ||
| providers: Record<string, any>; | ||
| }; | ||
| type A = Record<string, any>; | ||
| type B = { hello: 'world' }; | ||
| type C = A & B; | ||
| const c: C = null as any as C; |
| import type { HandlerContainer, Method } from '../../h11.ts'; | ||
| export class Node { | ||
| segment: string; | ||
| // data | ||
| handlers: Partial<Record<Method, HandlerContainer>> = {}; | ||
| children: Node[] = []; | ||
| wildParent: boolean = false; | ||
| parent?: Node; | ||
| constructor({ segment, parent }: { segment: string; parent?: Node }) { | ||
| this.segment = segment; | ||
| this.parent = parent; | ||
| } | ||
| } | ||
| export const addNodeToChildren = (parent: Node, node: Node) => { | ||
| const children = parent.children; | ||
| if (children.length === 0) { | ||
| children.push(node); | ||
| return; | ||
| } | ||
| if (node.segment === '**') { | ||
| children.push(node); | ||
| return; | ||
| } else if (node.segment[0] === ':') { | ||
| for (let i = children.length - 1; i >= 0; i--) { | ||
| const compareNode = children[i]; | ||
| if (compareNode.segment === '**') { | ||
| continue; | ||
| } | ||
| children.splice(i + 1, 0, node); | ||
| return; | ||
| } | ||
| children.unshift(node); | ||
| } else { | ||
| for (let i = children.length - 1; i >= 0; i--) { | ||
| const compareNode = children[i]; | ||
| if (compareNode.segment === '**' || compareNode.segment[0] === ':') { | ||
| continue; | ||
| } | ||
| children.splice(i + 1, 0, node); | ||
| return; | ||
| } | ||
| children.unshift(node); | ||
| } | ||
| }; |
| import { describe, it, expect } from 'bun:test'; | ||
| import { Radix } from './radix.ts'; | ||
| const handlerContainer = { | ||
| handler: async () => new Response(), | ||
| plugins: [], | ||
| }; | ||
| describe('radix', () => { | ||
| it('add', () => { | ||
| const radix = new Radix(); | ||
| const node = radix.add('/hello/:name/world', 'GET', handlerContainer); | ||
| // node.handlers.push(handler); | ||
| expect(radix.root.children.length).toBe(1); | ||
| const helloNode = radix.root.children[0]; | ||
| expect(helloNode.segment).toBe('hello'); | ||
| expect(helloNode.children.length).toBe(1); | ||
| const nameNode = helloNode.children[0]; | ||
| expect(nameNode.segment).toBe(':name'); | ||
| expect(nameNode.children.length).toBe(1); | ||
| const worldNode = nameNode.children[0]; | ||
| expect(worldNode.segment).toBe('world'); | ||
| expect(worldNode.children.length).toBe(0); | ||
| expect(node).toBe(worldNode); | ||
| radix.add('/hello/:name/world2', 'GET', handlerContainer); | ||
| expect(nameNode.children.length).toBe(2); | ||
| const world2Node = nameNode.children[1]; | ||
| expect(world2Node.segment).toBe('world2'); | ||
| expect(world2Node.children.length).toBe(0); | ||
| }); | ||
| it('find', () => { | ||
| const radix = new Radix(); | ||
| const worldNode = radix.add('/hello/:name/world', 'GET', handlerContainer); | ||
| const findResult = radix.find('/hello/aleksey/world'); | ||
| expect(findResult).not.toBe(null); | ||
| expect(findResult!.node).toBe(worldNode); | ||
| expect(findResult!.params.name).toBe('aleksey'); | ||
| const cityNode = radix.add('/hello2/:name/:city', 'GET', handlerContainer); | ||
| const findResult2 = radix.find('/hello2/aleksey/london'); | ||
| expect(findResult2).not.toBe(null); | ||
| expect(findResult2!.node).toBe(cityNode); | ||
| expect(findResult2!.params.name).toBe('aleksey'); | ||
| expect(findResult2!.params.city).toBe('london'); | ||
| const findResult3 = radix.find('/sdsdsdsdsds'); | ||
| expect(findResult3).toBe(null); | ||
| }); | ||
| it('find wild', () => { | ||
| const radix = new Radix(); | ||
| radix.add('/hello/world/:name', 'GET', handlerContainer); | ||
| radix.add('/hello/**', 'GET', handlerContainer); | ||
| let result = radix.find('/hello/world/', 'GET')!; | ||
| expect(result.node.segment).toBe('**'); | ||
| result = radix.find('/hello/world/aleksey', 'GET')!; | ||
| expect(result.node.segment[0]).toBe(':'); | ||
| result = radix.find('/hello', 'GET')!; | ||
| expect(result.node.segment).toBe('**'); | ||
| const resultNull = radix.find('/hello123', 'GET'); | ||
| expect(resultNull).toBe(null); | ||
| }); | ||
| it('find named and regular', () => { | ||
| const radix = new Radix(); | ||
| radix.add('/hello/world/:name', 'GET', handlerContainer); | ||
| radix.add('/hello/world/aleksey', 'GET', handlerContainer); | ||
| let result = radix.find('/hello/world/aleksey')!; | ||
| expect(result.node.segment).toBe('aleksey'); | ||
| }); | ||
| it('find named and wild', () => { | ||
| const radix = new Radix(); | ||
| radix.add('/hello/world/**', 'GET', handlerContainer); | ||
| radix.add('/hello/world/:name', 'GET', handlerContainer); | ||
| let result = radix.find('/hello/world/aleksey')!; | ||
| expect(result.node.segment).toBe(':name'); | ||
| }); | ||
| }); |
| import type { HandlerContainer, Method } from '../h11.ts'; | ||
| import { Node, addNodeToChildren } from './node/node.ts'; | ||
| type Params = Record<string, string>; | ||
| // /hello/world/:name/:family | ||
| export class Radix { | ||
| root = new Node({ segment: '' }); | ||
| find(path: string, method: Method = 'GET') { | ||
| const segments = path.split('/'); | ||
| const params: Params = {}; | ||
| let currentNode = this.root; | ||
| let lastWild: { | ||
| handler: HandlerContainer; | ||
| params: Params; | ||
| node: Node; | ||
| } | null = null; | ||
| outer: for (let i = 0; i < segments.length; i++) { | ||
| const segment = segments[i]; | ||
| if (currentNode.wildParent === true) { | ||
| const wild = currentNode.children[currentNode.children.length - 1]; | ||
| const wildHandlerContainer = wild.handlers[method]; | ||
| if (wildHandlerContainer) { | ||
| lastWild = { | ||
| node: wild, | ||
| handler: wildHandlerContainer, | ||
| params: { ...params, wild: segments.slice(i).join('/') }, | ||
| }; | ||
| } | ||
| } | ||
| if (currentNode.segment[0] === ':') { | ||
| params[currentNode.segment.slice(1)] = segment; | ||
| } | ||
| const isLastSegment = i + 1 === segments.length; | ||
| if (isLastSegment) { | ||
| break; | ||
| } | ||
| if (currentNode.children.length === 0) { | ||
| // no node children in tree | ||
| return lastWild; | ||
| } | ||
| for (const child of currentNode.children) { | ||
| const nextSegment = segments[i + 1]; | ||
| if (child.segment === nextSegment) { | ||
| currentNode = child; | ||
| continue outer; | ||
| } | ||
| if (child.segment[0] === ':' && nextSegment !== '') { | ||
| currentNode = child; | ||
| continue outer; | ||
| } | ||
| } | ||
| // no children node with target name | ||
| return lastWild; | ||
| } | ||
| const handlerContainer = currentNode.handlers[method]; | ||
| if (!handlerContainer) { | ||
| return lastWild; | ||
| } | ||
| // handle last segment | ||
| return { | ||
| node: currentNode, | ||
| params, | ||
| handler: handlerContainer, | ||
| }; | ||
| } | ||
| // hello/world/:name | ||
| add(pattern: string, method: Method = 'GET', handler: HandlerContainer) { | ||
| const patternSegments = pattern.slice(1).split('/'); | ||
| let currentNode = this.root; | ||
| outer: for (let i = 0; i < patternSegments.length; i++) { | ||
| const segment = patternSegments[i]; | ||
| for (const child of currentNode.children) { | ||
| if (child.segment === segment) { | ||
| currentNode = child; | ||
| continue outer; | ||
| } | ||
| } | ||
| const newNode = new Node({ segment, parent: currentNode }); | ||
| addNodeToChildren(currentNode, newNode); | ||
| if (segment === '**') { | ||
| currentNode.wildParent = true; | ||
| // setWildToChildren(currentNode, method, ); | ||
| } | ||
| currentNode = newNode; | ||
| } | ||
| currentNode.handlers[method] = handler; | ||
| return currentNode; | ||
| } | ||
| } |
| { | ||
| "compilerOptions": { | ||
| // Enable latest features | ||
| "lib": ["ESNext", "DOM"], | ||
| "target": "ESNext", | ||
| "module": "ESNext", | ||
| "moduleDetection": "force", | ||
| "jsx": "react-jsx", | ||
| "allowJs": true, | ||
| // Bundler mode | ||
| "moduleResolution": "bundler", | ||
| "allowImportingTsExtensions": true, | ||
| "verbatimModuleSyntax": true, | ||
| "noEmit": true, | ||
| // Best practices | ||
| "strict": true, | ||
| "skipLibCheck": true, | ||
| "noFallthroughCasesInSwitch": true, | ||
| // Some stricter flags (disabled by default) | ||
| "noUnusedLocals": false, | ||
| "noUnusedParameters": false, | ||
| "noPropertyAccessFromIndexSignature": false | ||
| } | ||
| } |
| { | ||
| "extends": "./tsconfig.types.json", | ||
| "include": ["src/providers/bun.ts"] | ||
| } |
| { | ||
| "extends": "./tsconfig.json", | ||
| "compilerOptions": { | ||
| "emitDeclarationOnly": true, | ||
| "outDir": "./dist/types", | ||
| "declaration": true, | ||
| "noEmit": false | ||
| }, | ||
| "include": ["src/h11.ts"] | ||
| } |
| { | ||
| "extends": "./tsconfig.types.json", | ||
| "include": ["src/providers/node.ts"] | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Network access
Supply chain riskThis module accesses the network.
Found 2 instances
0
-100%1
-66.67%10592
-48.6%4
100%10
-47.37%355
-32.25%- Removed
- Removed
- Removed
- Removed