Skip to content

feat: add support for wildcards in subscriptions #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions source/event_hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,9 @@ export class EventHub {
/**
* Register to *subscription* events.
*
* @param {String} subscription Expression to subscribe on. Currently,
* only "topic=value" expressions are
* supported.
* @param {String} subscription Expression to subscribe on. This can
* be in the format of "topic=value" or
* include a wildcard like "topic=ftrack.*".
* @param {Function} callback Function to be called when an event
* matching the subscription is returned.
* @param {Object} [metadata] Optional information about subscriber.
Expand All @@ -391,6 +391,9 @@ export class EventHub {
callback: EventCallback,
metadata?: SubscriberMetadata
): string {
if (typeof callback !== "function") {
throw new Error("Callback must be a function.");
}
const subscriber = this._addSubscriber(subscription, callback, metadata);
this._notifyServerAboutSubscriber(subscriber);
return subscriber.metadata.id;
Expand Down Expand Up @@ -520,9 +523,7 @@ export class EventHub {
/**
* Return if *subscriber* is interested in *event*.
*
* Only expressions on the format topic=value is supported.
*
* TODO: Support the full event expression format.
* Expressions on the format topic=value is supported, including wildcard support.
*
* @param {Object} subscriber
* @param {Object} eventPayload
Expand All @@ -533,9 +534,20 @@ export class EventHub {
eventPayload: EventPayload
) {
const topic = this._getExpressionTopic(subscriber.subscription);
if (topic === eventPayload.topic) {

// Support for wildcard matching in topic.
if (topic.endsWith("*")) {
const baseTopic = topic.slice(0, -1); // remove the wildcard character
if (typeof eventPayload.topic !== "string") {
return false;
}
if (eventPayload.topic.startsWith(baseTopic)) {
return true;
}
} else if (topic === eventPayload.topic) {
return true;
}

return false;
}

Expand Down
155 changes: 155 additions & 0 deletions test/event_hub.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { EventHub } from "../source/event_hub";
import { vi, describe, expect } from "vitest";

describe("EventHub", () => {
let eventHub;

beforeEach(() => {
eventHub = new EventHub("", "", "");
eventHub._socketIo = {
on: vi.fn(),
emit: vi.fn(),
socket: { connected: true },
};
});

afterEach(() => {
vi.clearAllMocks();
});

test("should correctly add subscribers", () => {
const topicSubscriberCallback = vi.fn();
const wildcardSubscriberCallback = vi.fn();

const topicSubscriberId = eventHub.subscribe(
"topic=ftrack.update",
topicSubscriberCallback
);
const wildcardSubscriberId = eventHub.subscribe(
"topic=ftrack.*",
wildcardSubscriberCallback
);

expect(typeof topicSubscriberId).toBe("string");
expect(eventHub.getSubscriberByIdentifier(topicSubscriberId).callback).toBe(
topicSubscriberCallback
);
expect(typeof wildcardSubscriberId).toBe("string");
expect(
eventHub.getSubscriberByIdentifier(wildcardSubscriberId).callback
).toBe(wildcardSubscriberCallback);
});

test("should not subscribe without a valid topic", () => {
const callback = vi.fn();
expect(() => eventHub.subscribe("", callback)).toThrow();
expect(() => eventHub.subscribe(null, callback)).toThrow();
expect(() => eventHub.subscribe(undefined, callback)).toThrow();
expect(() => eventHub.subscribe("*", callback)).toThrow();
expect(() =>
eventHub.subscribe("anything-except-topic", callback)
).toThrow();
});

test("should not subscribe without a valid callback", () => {
expect(() => eventHub.subscribe("topic=ftrack.update", null)).toThrow();
expect(() =>
eventHub.subscribe("topic=ftrack.update", "not a function")
).toThrow();
expect(() => eventHub.subscribe("topic=ftrack.update", {})).toThrow();
});

test("should correctly unsubscribe", () => {
const topicSubscriberCallback = vi.fn();
const wildcardSubscriberCallback = vi.fn();
const topicSubscriberId = eventHub.subscribe(
"topic=ftrack.update",
topicSubscriberCallback
);
const wildcardSubscriberId = eventHub.subscribe(
"topic=ftrack.*",
wildcardSubscriberCallback
);

const topicUnsubscribeSuccess = eventHub.unsubscribe(topicSubscriberId);
const wildcardUnsubscribeSuccess =
eventHub.unsubscribe(wildcardSubscriberId);

expect(topicUnsubscribeSuccess).toBe(true);
expect(wildcardUnsubscribeSuccess).toBe(true);
expect(eventHub.getSubscriberByIdentifier(topicSubscriberId)).toBe(null);
expect(eventHub.getSubscriberByIdentifier(wildcardSubscriberId)).toBe(null);
});

test("should not unsubscribe with an invalid ID", () => {
const invalid1 = eventHub.unsubscribe("invalid ID");
const invalid2 = eventHub.unsubscribe(null);
const invalid3 = eventHub.unsubscribe(undefined);

expect(invalid1).toBe(false);
expect(invalid2).toBe(false);
expect(invalid3).toBe(false);
});

test("should handle topic events", () => {
const callback = vi.fn();
const testEvent = { topic: "ftrack.test", data: {} };
eventHub.subscribe("topic=ftrack.*", callback);

eventHub._handle(testEvent);

expect(callback).toHaveBeenCalledWith(testEvent);
});

test("should handle wildcard events", () => {
const callback = vi.fn();
const testEvent = { topic: "ftrack.test", data: {} };
eventHub.subscribe("topic=ftrack.test", callback);

eventHub._handle(testEvent);

expect(callback).toHaveBeenCalledWith(testEvent);
});

test("should handle events with unexpected topics", () => {
const callback = vi.fn();
eventHub.subscribe("topic=ftrack.update", callback);
const testEvent = { topic: null, data: {} };
eventHub._handle(testEvent);
expect(callback).not.toHaveBeenCalled();
});

test("should handle events with unexpected topics", () => {
const callback = vi.fn();
eventHub.subscribe("topic=*", callback);
const testEvent = { topic: null, data: {} };
const testEvent2 = { topic: { topic: "topic" }, data: {} };
const testEvent3 = { topic: ["topic"], data: {} };

eventHub._handle(testEvent);
eventHub._handle(testEvent2);
eventHub._handle(testEvent3);

expect(callback).not.toHaveBeenCalled();
});

test("should handle events without data", () => {
const callback = vi.fn();
eventHub.subscribe("topic=ftrack.update", callback);
const testEvent = { topic: "ftrack.update" };
eventHub._handle(testEvent);
expect(callback).toHaveBeenCalledWith(testEvent);
});

test("Should not handle non subscribed events", () => {
const callback = vi.fn();
const testEvent = { topic: "test.topic", data: {} };
eventHub.subscribe("topic=ftrack.*", callback);
eventHub.subscribe("topic=ftrack.update", callback);
eventHub.subscribe("topic=test.topic.*", callback);

eventHub._handle(testEvent);

expect(callback).not.toHaveBeenCalledWith(testEvent);
});
});