Skip to content

Commit 4d84dfd

Browse files
authored
fix(transport-commons): Fix route placeholder registration and improve radix router performance (#2336)
1 parent 5d30876 commit 4d84dfd

2 files changed

Lines changed: 58 additions & 27 deletions

File tree

packages/transport-commons/src/routing/router.ts

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { stripSlashes } from '@feathersjs/commons';
2-
import { BadRequest } from '@feathersjs/errors';
32

43
export interface LookupData {
54
params: { [key: string]: string };
@@ -12,62 +11,74 @@ export interface LookupResult<T> extends LookupData {
1211
export class RouteNode<T = any> {
1312
data?: T;
1413
children: { [key: string]: RouteNode } = {};
15-
placeholder?: RouteNode;
14+
placeholders: RouteNode[] = [];
1615

17-
constructor (public name: string) {}
16+
constructor (public name: string, public depth: number) {}
1817

1918
insert (path: string[], data: T): RouteNode<T> {
20-
if (path.length === 0) {
19+
if (this.depth === path.length) {
20+
if (this.data !== undefined) {
21+
throw new Error(`Path ${path.join('/')} already exists`);
22+
}
23+
2124
this.data = data;
2225
return this;
2326
}
2427

25-
const [ current, ...rest ] = path;
28+
const current = path[this.depth];
29+
const nextDepth = this.depth + 1;
2630

2731
if (current.startsWith(':')) {
28-
const { placeholder } = this;
29-
const name = current.substring(1);
32+
// Insert a placeholder node like /messages/:id
33+
const placeholderName = current.substring(1);
34+
let placeholder = this.placeholders.find(p => p.name === placeholderName);
3035

3136
if (!placeholder) {
32-
this.placeholder = new RouteNode(name);
33-
} else if(placeholder.name !== name) {
34-
throw new BadRequest(`Can not add route with placeholder ':${name}' because placeholder ':${placeholder.name}' already exists`);
37+
placeholder = new RouteNode(placeholderName, nextDepth);
38+
this.placeholders.push(placeholder);
3539
}
3640

37-
return this.placeholder.insert(rest, data);
41+
return placeholder.insert(path, data);
3842
}
3943

40-
this.children[current] = this.children[current] || new RouteNode(current);
44+
const child = this.children[current] || new RouteNode(current, nextDepth);
45+
46+
this.children[current] = child;
4147

42-
return this.children[current].insert(rest, data);
48+
return child.insert(path, data);
4349
}
4450

4551
lookup (path: string[], info: LookupData): LookupResult<T>|null {
46-
if (path.length === 0) {
47-
return {
52+
if (path.length === this.depth) {
53+
return this.data === undefined ? null : {
4854
...info,
4955
data: this.data
5056
}
5157
}
5258

53-
const [ current, ...rest ] = path;
59+
const current = path[this.depth];
5460
const child = this.children[current];
5561

5662
if (child) {
57-
return child.lookup(rest, info);
63+
return child.lookup(path, info);
5864
}
5965

60-
if (this.placeholder) {
61-
info.params[this.placeholder.name] = current;
62-
return this.placeholder.lookup(rest, info);
66+
// This will return the first placeholder that matches early
67+
for(const placeholder of this.placeholders) {
68+
const result = placeholder.lookup(path, info);
69+
70+
if (result !== null) {
71+
result.params[placeholder.name] = current;
72+
return result;
73+
}
6374
}
6475

6576
return null;
6677
}
6778
}
6879

6980
export class Router<T> {
70-
root: RouteNode<T> = new RouteNode<T>('');
81+
constructor (public root: RouteNode<T> = new RouteNode<T>('', 0)) {}
7182

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

packages/transport-commons/test/routing/router.test.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ describe('router', () => {
4040

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

43+
assert.throws(() => r.insert('/hello/:id/you', 'two'), {
44+
message: 'Path hello/:id/you already exists'
45+
});
46+
4347
assert.deepStrictEqual(first, {
4448
params: { id: 'there' },
4549
data: 'one'
@@ -65,14 +69,30 @@ describe('router', () => {
6569
assert.strictEqual(r.lookup('hello/yes/they/here'), null);
6670
});
6771

68-
it('errors when placeholder in a path is different', () => {
72+
it('works with different placeholders in different paths (#2327)', () => {
6973
const r = new Router<string>();
7074

71-
assert.throws(() => {
72-
r.insert('/hello/:id', 'one');
73-
r.insert('/hello/:test/you', 'two');
74-
}, {
75-
message: 'Can not add route with placeholder \':test\' because placeholder \':id\' already exists'
75+
r.insert('/hello/:id', 'one');
76+
r.insert('/hello/:test/you', 'two');
77+
r.insert('/hello/:test/:two/hi/:three', 'three');
78+
r.insert('/hello/:test/:two/hi', 'four');
79+
80+
assert.deepStrictEqual(r.lookup('/hello/there'), {
81+
params: { id: 'there' },
82+
data: 'one'
83+
});
84+
assert.deepStrictEqual(r.lookup('/hello/there/you'), {
85+
params: { test: 'there' },
86+
data: 'two'
87+
});
88+
assert.strictEqual(r.lookup('/hello/there/bla'), null);
89+
assert.deepStrictEqual(r.lookup('/hello/there/maybe/hi'), {
90+
params: { test: 'there', two: 'maybe' },
91+
data: 'four'
92+
});
93+
assert.deepStrictEqual(r.lookup('/hello/there/maybe/hi/test'), {
94+
params: { three: 'test', two: 'maybe', test: 'there' },
95+
data: 'three'
7696
});
7797
});
7898
});

0 commit comments

Comments
 (0)