Skip to content

Commit 5f6fe3a

Browse files
committed
fix: preserve canonical listener keys when merging emitters
1 parent d870a77 commit 5f6fe3a

3 files changed

Lines changed: 123 additions & 4 deletions

File tree

src/emitter/EventEmitter.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,21 @@ export default class EventEmitter<M extends EventMap> {
177177
//skip
178178
continue;
179179
}
180+
//listener keys are already canonical in the source emitter.
181+
//Copy them directly so subclass on() overrides do not reinterpret
182+
//regex-backed route keys as fresh event patterns during merge.
183+
if (typeof this._listeners[event as keyof M] === 'undefined') {
184+
this._listeners[event as keyof M] = new Set<TaskItem<M[keyof M]>>();
185+
}
186+
const listeners = this._listeners[event as keyof M] as Set<
187+
TaskItem<M[keyof M]>
188+
>;
180189
//then loop the tasks
181190
for (const { item, priority } of tasks) {
182-
//listen to each task one by one
183-
this.on(event, item, priority);
191+
listeners.add({
192+
item: item as TaskAction<M[keyof M]>,
193+
priority
194+
});
184195
}
185196
}
186197
return this;
@@ -213,4 +224,4 @@ export default class EventEmitter<M extends EventMap> {
213224
}
214225
};
215226
}
216-
}
227+
}

tests/RouteEmitter.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,30 @@ describe('RouteEmitter Tests', () => {
135135
expect(triggered).to.deep.equal(['/shared/test']);
136136
expect(router.routes.size).to.equal(0);
137137
});
138+
139+
it('should preserve canonical regex listener keys when merging parameterized routes', async () => {
140+
const child = new Router<R, S>();
141+
const parent = new Router<R, S>();
142+
const event = '/^GET \\/users\\/([^/]+)\\/*$/g';
143+
const malformed = '/^\\/^GET \\/users\\/([^/]+)\\/([^/]+)$\\/g$/g';
144+
const triggered: string[] = [];
145+
146+
child.route('GET', '/users/:id', (req, res) => {
147+
triggered.push(req.path);
148+
res.body = 'matched';
149+
});
150+
151+
parent.use(child);
152+
const response = await parent.emit(
153+
'GET /users/42',
154+
{ path: '/users/42' },
155+
{}
156+
);
157+
158+
expect(response.code).to.equal(200);
159+
expect(triggered).to.deep.equal(['/users/42']);
160+
expect(parent.listeners[event]).to.be.instanceOf(Set);
161+
expect(parent.listeners[malformed]).to.be.undefined;
162+
expect(Array.from(parent.expressions.keys())).to.deep.equal([event]);
163+
});
138164
});

tests/Router.test.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,86 @@ describe('Router Tests', () => {
129129
}>;
130130
expect(Array.from(tasks.values())[1].item.name).to.equal('withName');
131131
})
132-
})
132+
133+
it('should preserve exact, parameterized, and fallback routes after use()', async () => {
134+
const child = new Router<R, S>();
135+
const parent = new Router<R, S>();
136+
const triggered: string[] = [];
137+
138+
child.route('GET', '/', (_req, res) => {
139+
triggered.push('root');
140+
res.body = 'root';
141+
});
142+
child.route('GET', '/login', (_req, res) => {
143+
triggered.push('login');
144+
res.body = 'login';
145+
});
146+
child.route('GET', '/user', (_req, res) => {
147+
triggered.push('user');
148+
res.body = 'user';
149+
});
150+
child.route('GET', '/user/:id', (req, res) => {
151+
triggered.push(`get:${req.data('id')}`);
152+
res.body = `get:${req.data('id')}`;
153+
});
154+
child.route('PUT', '/user/:id', (req, res) => {
155+
triggered.push(`put:${req.data('id')}`);
156+
res.body = `put:${req.data('id')}`;
157+
});
158+
child.route('DELETE', '/user/:id', (req, res) => {
159+
triggered.push(`delete:${req.data('id')}`);
160+
res.body = `delete:${req.data('id')}`;
161+
});
162+
child.route('ANY', '/files/**', (_req, res) => {
163+
triggered.push('wildcard');
164+
res.body = 'fallback';
165+
});
166+
167+
parent.use(child);
168+
169+
const rootReq = parent.request({ resource: { path: '/' } });
170+
const rootRes = parent.response({ resource: {} });
171+
await parent.emit('GET /', rootReq, rootRes);
172+
173+
const loginReq = parent.request({ resource: { path: '/login' } });
174+
const loginRes = parent.response({ resource: {} });
175+
await parent.emit('GET /login', loginReq, loginRes);
176+
177+
const userReq = parent.request({ resource: { path: '/user' } });
178+
const userRes = parent.response({ resource: {} });
179+
await parent.emit('GET /user', userReq, userRes);
180+
181+
const getReq = parent.request({ resource: { path: '/user/1' } });
182+
const getRes = parent.response({ resource: {} });
183+
await parent.emit('GET /user/1', getReq, getRes);
184+
185+
const putReq = parent.request({ resource: { path: '/user/1' } });
186+
const putRes = parent.response({ resource: {} });
187+
await parent.emit('PUT /user/1', putReq, putRes);
188+
189+
const deleteReq = parent.request({ resource: { path: '/user/1' } });
190+
const deleteRes = parent.response({ resource: {} });
191+
await parent.emit('DELETE /user/1', deleteReq, deleteRes);
192+
193+
const fallbackReq = parent.request({ resource: { path: '/files/missing/path' } });
194+
const fallbackRes = parent.response({ resource: {} });
195+
await parent.emit('PATCH /files/missing/path', fallbackReq, fallbackRes);
196+
197+
expect(triggered).to.deep.equal([
198+
'root',
199+
'login',
200+
'user',
201+
'get:1',
202+
'put:1',
203+
'delete:1',
204+
'wildcard'
205+
]);
206+
expect(rootRes.body).to.equal('root');
207+
expect(loginRes.body).to.equal('login');
208+
expect(userRes.body).to.equal('user');
209+
expect(getRes.body).to.equal('get:1');
210+
expect(putRes.body).to.equal('put:1');
211+
expect(deleteRes.body).to.equal('delete:1');
212+
expect(fallbackRes.body).to.equal('fallback');
213+
});
214+
})

0 commit comments

Comments
 (0)