Skip to content

Commit 9edd31f

Browse files
committed
[ADD] runtime/utils: optional validation to EventBus
Currently the event bus allows sending and listening to arbitrary events, I got got by that when I pushed a fix using `addEventListener` on a bus across an events renaming, and on the other side the fix did nothing anymore. Entirely my fault, but if the list of events sent on a bus is known and documented (e.g. a jsdoc has `@emits` tags) it would make sense for both the listening and the dispatching to also be validated. This proposal performs validation only when the eventbus is created: - in dev mode (which also requires being in a component context) - if an iterable of events is passed to the ctor The dev-mode check might be overkill but it seems like a good idea at least for an initial version, as the validation does have a cost however low, and validation errors can occur essentially anywhere.
1 parent c2728c9 commit 9edd31f

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

src/runtime/utils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { OwlError } from "../common/owl_error";
2+
import { ComponentNode, getCurrent } from "./component_node";
23
export type Callback = () => void;
34

45
/**
@@ -81,10 +82,52 @@ export function validateTarget(target: HTMLElement | ShadowRoot) {
8182
}
8283

8384
export class EventBus extends EventTarget {
85+
constructor(events?: string[]) {
86+
if (events) {
87+
let node: ComponentNode | null = null;
88+
try {
89+
node = getCurrent();
90+
} catch {}
91+
if (node?.app?.dev) {
92+
return new DebugEventBus(events);
93+
}
94+
}
95+
super();
96+
}
8497
trigger(name: string, payload?: any) {
8598
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
8699
}
87100
}
101+
class DebugEventBus extends EventBus {
102+
private events: Set<string>;
103+
constructor(events: string[]) {
104+
super();
105+
this.events = new Set(events);
106+
}
107+
addEventListener(
108+
type: string,
109+
listener: EventListenerOrEventListenerObject | null,
110+
options?: boolean | AddEventListenerOptions
111+
): void {
112+
if (!this.events.has(type)) {
113+
throw new OwlError(`EventBus: subscribing to unknown event '${type}'`);
114+
}
115+
super.addEventListener(type, listener, options);
116+
}
117+
trigger(name: string, payload?: any) {
118+
if (!this.events.has(name)) {
119+
throw new OwlError(`EventBus: triggering unknown event '${name}'`);
120+
}
121+
super.trigger(name, payload);
122+
}
123+
124+
dispatchEvent(event: Event): boolean {
125+
if (!this.events.has(event.type)) {
126+
throw new OwlError(`EventBus: dispatching unknown event '${event.type}'`);
127+
}
128+
return super.dispatchEvent(event);
129+
}
130+
}
88131

89132
export function whenReady(fn?: any): Promise<void> {
90133
return new Promise(function (resolve) {

tests/utils.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
2-
import { nextMicroTick } from "./helpers";
2+
import { makeTestFixture, nextMicroTick } from "./helpers";
3+
import { getCurrent } from "../src/runtime/component_node";
4+
import { Component, mount, xml } from "../src";
35

46
describe("event bus behaviour", () => {
57
test("can subscribe and be notified", () => {
@@ -33,6 +35,66 @@ describe("event bus behaviour", () => {
3335
bus.addEventListener("event", (ev: any) => expect(ev.detail).toBe("hello world"));
3436
bus.trigger("event", "hello world");
3537
});
38+
39+
test("events are not validated if the bus is created outside of dev mode", async () => {
40+
let bus_empty: EventBus | null = null;
41+
class Root extends Component {
42+
static template = xml`<div/>`;
43+
44+
setup() {
45+
getCurrent(); // checks that we're in a component context
46+
47+
bus_empty = new EventBus([]);
48+
}
49+
}
50+
await mount(Root, makeTestFixture());
51+
52+
bus_empty!.addEventListener("a", () => {});
53+
bus_empty!.trigger("a");
54+
bus_empty!.dispatchEvent(new CustomEvent("a"));
55+
});
56+
test("events are validated if the bus is created in dev mode & events are provided", async () => {
57+
let bus: EventBus | null = null;
58+
let bus_empty: EventBus | null = null;
59+
let bbus_no_validation: EventBus | null = null;
60+
class Root extends Component {
61+
static template = xml`<div/>`;
62+
63+
setup() {
64+
getCurrent(); // checks that we're in a component context
65+
66+
bus = new EventBus(["a", "b"]);
67+
bus_empty = new EventBus([]);
68+
bbus_no_validation = new EventBus();
69+
}
70+
}
71+
72+
await mount(Root, makeTestFixture(), { test: true });
73+
74+
bbus_no_validation!.addEventListener("c", () => {});
75+
bbus_no_validation!.trigger("c");
76+
bbus_no_validation!.dispatchEvent(new CustomEvent("c"));
77+
78+
bus!.addEventListener("a", () => {});
79+
bus!.trigger("a");
80+
bus!.dispatchEvent(new CustomEvent("a"));
81+
82+
expect(() => bus!.addEventListener("c", () => {})).toThrow(
83+
"EventBus: subscribing to unknown event 'c'"
84+
);
85+
expect(() => bus!.trigger("c")).toThrow("EventBus: triggering unknown event 'c'");
86+
expect(() => bus!.dispatchEvent(new CustomEvent("c"))).toThrow(
87+
"EventBus: dispatching unknown event 'c'"
88+
);
89+
90+
expect(() => bus_empty!.addEventListener("a", () => {})).toThrow(
91+
"EventBus: subscribing to unknown event 'a'"
92+
);
93+
expect(() => bus_empty!.trigger("a")).toThrow("EventBus: triggering unknown event 'a'");
94+
expect(() => bus_empty!.dispatchEvent(new CustomEvent("a"))).toThrow(
95+
"EventBus: dispatching unknown event 'a'"
96+
);
97+
});
3698
});
3799

38100
describe("batched", () => {

0 commit comments

Comments
 (0)