-
-
Notifications
You must be signed in to change notification settings - Fork 754
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(transport-commons): New built-in high performance radix router (#…
- Loading branch information
Showing
7 changed files
with
220 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: {} }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
}); | ||
}); | ||
}); |