Skip to content

Commit 9b161ba

Browse files
gismyajimmycallin
andauthored
feat: add support for wildcards in subscriptions (#106)
* feat: add support for wildcards in subscriptions * Tests --------- Co-authored-by: Jimmy Callin <jimmy.callin@ftrack.com>
1 parent 959f1da commit 9b161ba

File tree

2 files changed

+174
-7
lines changed

2 files changed

+174
-7
lines changed

source/event_hub.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,9 @@ export class EventHub {
378378
/**
379379
* Register to *subscription* events.
380380
*
381-
* @param {String} subscription Expression to subscribe on. Currently,
382-
* only "topic=value" expressions are
383-
* supported.
381+
* @param {String} subscription Expression to subscribe on. This can
382+
* be in the format of "topic=value" or
383+
* include a wildcard like "topic=ftrack.*".
384384
* @param {Function} callback Function to be called when an event
385385
* matching the subscription is returned.
386386
* @param {Object} [metadata] Optional information about subscriber.
@@ -391,6 +391,9 @@ export class EventHub {
391391
callback: EventCallback,
392392
metadata?: SubscriberMetadata
393393
): string {
394+
if (typeof callback !== "function") {
395+
throw new Error("Callback must be a function.");
396+
}
394397
const subscriber = this._addSubscriber(subscription, callback, metadata);
395398
this._notifyServerAboutSubscriber(subscriber);
396399
return subscriber.metadata.id;
@@ -520,9 +523,7 @@ export class EventHub {
520523
/**
521524
* Return if *subscriber* is interested in *event*.
522525
*
523-
* Only expressions on the format topic=value is supported.
524-
*
525-
* TODO: Support the full event expression format.
526+
* Expressions on the format topic=value is supported, including wildcard support.
526527
*
527528
* @param {Object} subscriber
528529
* @param {Object} eventPayload
@@ -533,9 +534,20 @@ export class EventHub {
533534
eventPayload: EventPayload
534535
) {
535536
const topic = this._getExpressionTopic(subscriber.subscription);
536-
if (topic === eventPayload.topic) {
537+
538+
// Support for wildcard matching in topic.
539+
if (topic.endsWith("*")) {
540+
const baseTopic = topic.slice(0, -1); // remove the wildcard character
541+
if (typeof eventPayload.topic !== "string") {
542+
return false;
543+
}
544+
if (eventPayload.topic.startsWith(baseTopic)) {
545+
return true;
546+
}
547+
} else if (topic === eventPayload.topic) {
537548
return true;
538549
}
550+
539551
return false;
540552
}
541553

test/event_hub.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { EventHub } from "../source/event_hub";
2+
import { vi, describe, expect } from "vitest";
3+
4+
describe("EventHub", () => {
5+
let eventHub;
6+
7+
beforeEach(() => {
8+
eventHub = new EventHub("", "", "");
9+
eventHub._socketIo = {
10+
on: vi.fn(),
11+
emit: vi.fn(),
12+
socket: { connected: true },
13+
};
14+
});
15+
16+
afterEach(() => {
17+
vi.clearAllMocks();
18+
});
19+
20+
test("should correctly add subscribers", () => {
21+
const topicSubscriberCallback = vi.fn();
22+
const wildcardSubscriberCallback = vi.fn();
23+
24+
const topicSubscriberId = eventHub.subscribe(
25+
"topic=ftrack.update",
26+
topicSubscriberCallback
27+
);
28+
const wildcardSubscriberId = eventHub.subscribe(
29+
"topic=ftrack.*",
30+
wildcardSubscriberCallback
31+
);
32+
33+
expect(typeof topicSubscriberId).toBe("string");
34+
expect(eventHub.getSubscriberByIdentifier(topicSubscriberId).callback).toBe(
35+
topicSubscriberCallback
36+
);
37+
expect(typeof wildcardSubscriberId).toBe("string");
38+
expect(
39+
eventHub.getSubscriberByIdentifier(wildcardSubscriberId).callback
40+
).toBe(wildcardSubscriberCallback);
41+
});
42+
43+
test("should not subscribe without a valid topic", () => {
44+
const callback = vi.fn();
45+
expect(() => eventHub.subscribe("", callback)).toThrow();
46+
expect(() => eventHub.subscribe(null, callback)).toThrow();
47+
expect(() => eventHub.subscribe(undefined, callback)).toThrow();
48+
expect(() => eventHub.subscribe("*", callback)).toThrow();
49+
expect(() =>
50+
eventHub.subscribe("anything-except-topic", callback)
51+
).toThrow();
52+
});
53+
54+
test("should not subscribe without a valid callback", () => {
55+
expect(() => eventHub.subscribe("topic=ftrack.update", null)).toThrow();
56+
expect(() =>
57+
eventHub.subscribe("topic=ftrack.update", "not a function")
58+
).toThrow();
59+
expect(() => eventHub.subscribe("topic=ftrack.update", {})).toThrow();
60+
});
61+
62+
test("should correctly unsubscribe", () => {
63+
const topicSubscriberCallback = vi.fn();
64+
const wildcardSubscriberCallback = vi.fn();
65+
const topicSubscriberId = eventHub.subscribe(
66+
"topic=ftrack.update",
67+
topicSubscriberCallback
68+
);
69+
const wildcardSubscriberId = eventHub.subscribe(
70+
"topic=ftrack.*",
71+
wildcardSubscriberCallback
72+
);
73+
74+
const topicUnsubscribeSuccess = eventHub.unsubscribe(topicSubscriberId);
75+
const wildcardUnsubscribeSuccess =
76+
eventHub.unsubscribe(wildcardSubscriberId);
77+
78+
expect(topicUnsubscribeSuccess).toBe(true);
79+
expect(wildcardUnsubscribeSuccess).toBe(true);
80+
expect(eventHub.getSubscriberByIdentifier(topicSubscriberId)).toBe(null);
81+
expect(eventHub.getSubscriberByIdentifier(wildcardSubscriberId)).toBe(null);
82+
});
83+
84+
test("should not unsubscribe with an invalid ID", () => {
85+
const invalid1 = eventHub.unsubscribe("invalid ID");
86+
const invalid2 = eventHub.unsubscribe(null);
87+
const invalid3 = eventHub.unsubscribe(undefined);
88+
89+
expect(invalid1).toBe(false);
90+
expect(invalid2).toBe(false);
91+
expect(invalid3).toBe(false);
92+
});
93+
94+
test("should handle topic events", () => {
95+
const callback = vi.fn();
96+
const testEvent = { topic: "ftrack.test", data: {} };
97+
eventHub.subscribe("topic=ftrack.*", callback);
98+
99+
eventHub._handle(testEvent);
100+
101+
expect(callback).toHaveBeenCalledWith(testEvent);
102+
});
103+
104+
test("should handle wildcard events", () => {
105+
const callback = vi.fn();
106+
const testEvent = { topic: "ftrack.test", data: {} };
107+
eventHub.subscribe("topic=ftrack.test", callback);
108+
109+
eventHub._handle(testEvent);
110+
111+
expect(callback).toHaveBeenCalledWith(testEvent);
112+
});
113+
114+
test("should handle events with unexpected topics", () => {
115+
const callback = vi.fn();
116+
eventHub.subscribe("topic=ftrack.update", callback);
117+
const testEvent = { topic: null, data: {} };
118+
eventHub._handle(testEvent);
119+
expect(callback).not.toHaveBeenCalled();
120+
});
121+
122+
test("should handle events with unexpected topics", () => {
123+
const callback = vi.fn();
124+
eventHub.subscribe("topic=*", callback);
125+
const testEvent = { topic: null, data: {} };
126+
const testEvent2 = { topic: { topic: "topic" }, data: {} };
127+
const testEvent3 = { topic: ["topic"], data: {} };
128+
129+
eventHub._handle(testEvent);
130+
eventHub._handle(testEvent2);
131+
eventHub._handle(testEvent3);
132+
133+
expect(callback).not.toHaveBeenCalled();
134+
});
135+
136+
test("should handle events without data", () => {
137+
const callback = vi.fn();
138+
eventHub.subscribe("topic=ftrack.update", callback);
139+
const testEvent = { topic: "ftrack.update" };
140+
eventHub._handle(testEvent);
141+
expect(callback).toHaveBeenCalledWith(testEvent);
142+
});
143+
144+
test("Should not handle non subscribed events", () => {
145+
const callback = vi.fn();
146+
const testEvent = { topic: "test.topic", data: {} };
147+
eventHub.subscribe("topic=ftrack.*", callback);
148+
eventHub.subscribe("topic=ftrack.update", callback);
149+
eventHub.subscribe("topic=test.topic.*", callback);
150+
151+
eventHub._handle(testEvent);
152+
153+
expect(callback).not.toHaveBeenCalledWith(testEvent);
154+
});
155+
});

0 commit comments

Comments
 (0)