Skip to content

Commit

Permalink
feat(transport-commons): New built-in high performance radix router (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl authored Jan 5, 2021
1 parent 6b80265 commit 6d18065
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 54 deletions.
4 changes: 2 additions & 2 deletions packages/socketio-client/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ describe('@feathersjs/socketio-client', () => {

it('return 404 for non-existent service', async () => {
try {
await app.service('not-me').create({});
await app.service('not/me').create({});
assert.fail('Should never get here');
} catch(e) {
assert.strictEqual(e.message, 'Service \'not-me\' not found')
assert.strictEqual(e.message, 'Service \'not/me\' not found')
}
});

Expand Down
3 changes: 1 addition & 2 deletions packages/transport-commons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@
"@feathersjs/commons": "^5.0.0-pre.1",
"@feathersjs/errors": "^5.0.0-pre.1",
"debug": "^4.3.1",
"lodash": "^4.17.20",
"radix-router": "^3.0.1"
"lodash": "^4.17.20"
},
"devDependencies": {
"@feathersjs/feathers": "^5.0.0-pre.1",
Expand Down
42 changes: 0 additions & 42 deletions packages/transport-commons/src/routing.ts

This file was deleted.

45 changes: 45 additions & 0 deletions packages/transport-commons/src/routing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Application, Service } from '@feathersjs/feathers';
import { Router } from './router';

declare module '@feathersjs/feathers/lib/declarations' {
interface RouteLookup {
service: Service<any>,
params: { [key: string]: string }
}

interface Application<ServiceTypes> { // eslint-disable-line
routes: Router<any>;
lookup (path: string): RouteLookup;
}
}

export * from './router';

export const routing = () => (app: Application) => {
if (typeof app.lookup === 'function') {
return;
}

const routes = new Router();

Object.assign(app, {
routes,
lookup (this: Application, path: string) {
const result = this.routes.lookup(path);

if (result !== null) {
const { params, data: service } = result;

return { params, service };
}

return result;
}
});

// Add a mixin that registers a service on the router
app.mixins.push((service: Service<any>, path: string) => {
app.routes.insert(path, service);
app.routes.insert(`${path}/:__id`, service);
});
};
87 changes: 87 additions & 0 deletions packages/transport-commons/src/routing/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { stripSlashes } from '@feathersjs/commons';
import { BadRequest } from '@feathersjs/errors';

export interface LookupData {
params: { [key: string]: string };
}

export interface LookupResult<T> extends LookupData {
data?: T;
}

export class RouteNode<T = any> {
data?: T;
children: { [key: string]: RouteNode } = {};
placeholder?: RouteNode;

constructor (public name: string) {}

insert (path: string[], data: T): RouteNode<T> {
if (path.length === 0) {
this.data = data;
return this;
}

const [ current, ...rest ] = path;

if (current.startsWith(':')) {
const { placeholder } = this;
const name = current.substring(1);

if (!placeholder) {
this.placeholder = new RouteNode(name);
} else if(placeholder.name !== name) {
throw new BadRequest(`Can not add route with placeholder ':${name}' because placeholder ':${placeholder.name}' already exists`);
}

return this.placeholder.insert(rest, data);
}

this.children[current] = this.children[current] || new RouteNode(current);

return this.children[current].insert(rest, data);
}

lookup (path: string[], info: LookupData): LookupResult<T>|null {
if (path.length === 0) {
return {
...info,
data: this.data
}
}

const [ current, ...rest ] = path;
const child = this.children[current];

if (child) {
return child.lookup(rest, info);
}

if (this.placeholder) {
info.params[this.placeholder.name] = current;
return this.placeholder.lookup(rest, info);
}

return null;
}
}

export class Router<T> {
root: RouteNode<T> = new RouteNode<T>('');

getPath (path: string) {
return stripSlashes(path).split('/');
}

insert (path: string, data: T) {
return this.root.insert(this.getPath(path), data);
}

lookup (path: string) {
if (typeof path !== 'string') {
return null;
}

return this.root.lookup(this.getPath(path), { params: {} });
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from 'assert';
import feathers, { Application } from '@feathersjs/feathers';
import { routing, ROUTER } from '../src/routing';
import { routing } from '../../src/routing';

describe('app.router', () => {
describe('app.routes', () => {
let app: Application;

beforeEach(() => {
Expand All @@ -19,10 +19,9 @@ describe('app.router', () => {
feathers().configure(routing()).configure(routing());
});

it('has app.lookup and ROUTER symbol', () => {
it('has app.lookup and app.routes', () => {
assert.strictEqual(typeof app.lookup, 'function');
// @ts-ignore
assert.ok(app[ROUTER]);
assert.ok(app.routes);
});

it('returns null when nothing is found', () => {
Expand All @@ -40,7 +39,7 @@ describe('app.router', () => {
it('can look up and strips slashes', () => {
const result = app.lookup('my/service');

assert.strictEqual(result.service, app.service('/my/service'));
assert.strictEqual(result.service, app.service('/my/service/'));
});

it('can look up with id', () => {
Expand All @@ -56,8 +55,8 @@ describe('app.router', () => {
const path = '/test/:first/my/:second';

app.use(path, {
get (id: string|number) {
return Promise.resolve({ id });
async get (id: string|number) {
return { id };
}
});

Expand Down
78 changes: 78 additions & 0 deletions packages/transport-commons/test/routing/router.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import assert from 'assert';
import { Router } from '../../src/routing';

describe('router', () => {
it('can lookup and insert a simple path and returns null for invalid path', () => {
const r = new Router<string>();

r.insert('/hello/there/you', 'test');

const result = r.lookup('hello/there/you/');

assert.deepStrictEqual(result, {
params: {},
data: 'test'
});

assert.strictEqual(r.lookup('not/there'), null);
assert.strictEqual(r.lookup('not-me'), null);
});

it('can insert data at the root', () => {
const r = new Router<string>();

r.insert('', 'hi');

const result = r.lookup('/');

assert.deepStrictEqual(result, {
params: {},
data: 'hi'
});
});

it('can insert with placeholder and has proper specificity', () => {
const r = new Router<string>();

r.insert('/hello/:id', 'one');
r.insert('/hello/:id/you', 'two');
r.insert('/hello/:id/:other', 'three');

const first = r.lookup('hello/there/');

assert.deepStrictEqual(first, {
params: { id: 'there' },
data: 'one'
});

const second = r.lookup('hello/yes/you');

assert.deepStrictEqual(second, {
params: { id: 'yes' },
data: 'two'
});

const third = r.lookup('hello/yes/they');

assert.deepStrictEqual(third, {
params: {
id: 'yes',
other: 'they'
},
data: 'three'
});

assert.strictEqual(r.lookup('hello/yes/they/here'), null);
});

it('errors when placeholder in a path is different', () => {
const r = new Router<string>();

assert.throws(() => {
r.insert('/hello/:id', 'one');
r.insert('/hello/:test/you', 'two');
}, {
message: 'Can not add route with placeholder \':test\' because placeholder \':id\' already exists'
});
});
});

0 comments on commit 6d18065

Please # to comment.