Summary
When a handler is registered on an event name that contains * (e.g. *, user.*), EventBus stores it in wildcardHandlers rather than handlers. Emitting that exact same literal name then triggers both:
- the wildcard match (because
* matches any event, including itself), and
- the exact-match wildcard entry,
so each registered handler fires twice. The same problem applies to any pattern literal — registering on "foo.*" and emitting "foo.*" double-fires.
Reproduction
```ts
import { Runtime } from 'writmint';
const rt = new Runtime();
const calls: number[] = [];
rt.registerPlugin({
name: 'p', version: '1.0.0',
setup: async (ctx) => {
ctx.events.on('', () => calls.push(1));
ctx.events.on('', () => calls.push(2));
},
});
await rt.initialize();
rt.getContext().events.emit('*');
console.log(calls.length); // 4 — expected 2
```
Where
- `src/event-bus.ts` — `emit` walks both `handlers` and `wildcardHandlers` and a literal-pattern handler matches both paths for its own name.
- See `tests/property/event-delivery.property.test.ts` (top-of-file comment) — the property suite excludes `*` from generated event names to work around this.
Suggested fix
In `emit`, when iterating wildcard handlers, skip entries whose pattern equals the emitted event name and was already delivered via the exact-match path; or, equivalently, route a literal pattern (`isWildcard(pattern) && pattern === eventName`) through one path only.
Acceptance
- The reproduction above prints `2`.
- Remove the `.filter((s) => !s.includes('*'))` workaround on the `eventNameArb` generator in `tests/property/event-delivery.property.test.ts` and the suite stays green across many seeds.
Found
While running the prepublish gate for v0.1.0; fast-check counterexample `[2, "*"]` on seed `-1796970649`.
Summary
When a handler is registered on an event name that contains
*(e.g.*,user.*),EventBusstores it inwildcardHandlersrather thanhandlers. Emitting that exact same literal name then triggers both:*matches any event, including itself), andso each registered handler fires twice. The same problem applies to any pattern literal — registering on
"foo.*"and emitting"foo.*"double-fires.Reproduction
```ts
import { Runtime } from 'writmint';
const rt = new Runtime();
const calls: number[] = [];
rt.registerPlugin({
name: 'p', version: '1.0.0',
setup: async (ctx) => {
ctx.events.on('', () => calls.push(1));
ctx.events.on('', () => calls.push(2));
},
});
await rt.initialize();
rt.getContext().events.emit('*');
console.log(calls.length); // 4 — expected 2
```
Where
Suggested fix
In `emit`, when iterating wildcard handlers, skip entries whose pattern equals the emitted event name and was already delivered via the exact-match path; or, equivalently, route a literal pattern (`isWildcard(pattern) && pattern === eventName`) through one path only.
Acceptance
Found
While running the prepublish gate for v0.1.0; fast-check counterexample `[2, "*"]` on seed `-1796970649`.