Share one connect and close listener between subscribers

Every EventSubscriber added its own connect and close listener to the
connector, and Link caps the connector at 20 listeners per event. The
web Control and App use 13 close listeners, so the 8th plugin
subscriber made the browser log MaxListenersExceededWarning on every
page load. Subscribers on a connector now share one listener for each
event, which leaves the cap in place for everything else.
This commit is contained in:
Danielv123 committed 2026-10-31 23:07:34 +01:00
1 parent c1826aea98
commit 4d6b9b52e3
2 files changed
+72 -7

No files matched your search

+31 -7
View File
@@ -455,6 +455,36 @@ export interface SubscribableValue {
isDeleted: boolean,
}
/** Subscribers created on each connector, they share one connect and one close listener on it */
const connectorSubscribers = new WeakMap<object, Set<EventSubscriber<any, any>>>();
/**
* Pass connect and close events of a connector on to a subscriber
* Uses one listener for all subscribers, so that plugins creating many subscribers do not go past the
* listener limit set on the connector.
* @param connector - connector of the link the subscriber was created on
* @param subscriber - subscriber to pass the events to
*/
function addConnectorSubscriber(connector: WebSocketBaseConnector, subscriber: EventSubscriber<any, any>) {
let subscribers = connectorSubscribers.get(connector);
if (!subscribers) {
const created = new Set<EventSubscriber<any, any>>();
connectorSubscribers.set(connector, created);
connector.on("connect", () => {
for (const entry of created) {
entry.handleConnectionEvent("connect");
}
});
connector.on("close", () => {
for (const entry of created) {
entry.handleConnectionEvent("close");
}
});
subscribers = created;
}
subscribers.add(subscriber);
}
/**
* Component for subscribing to and tracking updates of a remote resource
* Multiple handlers can be subscribed at the same time
@@ -483,13 +513,7 @@ export class EventSubscriber<E, S = null> {
) {
control.handle(Event, this._handleEvent.bind(this));
// The events below exist only on websocket connectors, we can't early return because of test mocking
const webSocketConnector = control.connector as WebSocketBaseConnector;
webSocketConnector.on("connect", () => {
this.handleConnectionEvent("connect");
});
webSocketConnector.on("close", () => {
this.handleConnectionEvent("close");
});
addConnectorSubscriber(control.connector as WebSocketBaseConnector, this);
// Only the web Control reports account changes
const accountControl = control as Link & { onAccountUpdate?: (handler: () => void) => void };
if (typeof accountControl.onAccountUpdate === "function") {
+41
View File
@@ -1021,6 +1021,47 @@ describe("lib/subscriptions", function() {
it("should handle the provided event", function() {
assert.equal(mockControl._eventHandlers.has(RegisteredEvent), true);
});
it("should share one connect and close listener on the connector", function() {
const connector = mockControl.connector;
const connectListeners = connector.listenerCount("connect");
const closeListeners = connector.listenerCount("close");
const subscribers = [
new lib.EventSubscriber(StringPermissionEvent, mockControl),
new lib.EventSubscriber(FunctionPermissionEvent, mockControl),
];
assert.equal(subscribers.length, 2);
assert.equal(connector.listenerCount("connect"), connectListeners);
assert.equal(connector.listenerCount("close"), closeListeners);
});
it("should pass connect and close to every subscriber on the connector", function() {
const subscribers = [
eventSubscriber,
new lib.EventSubscriber(StringPermissionEvent, mockControl),
new lib.EventSubscriber(FunctionPermissionEvent, mockControl),
];
const received = subscribers.map(() => []);
subscribers.forEach((subscriber, index) => {
subscriber.handleConnectionEvent = event => { received[index].push(event); };
});
mockControl.connector.emit("close");
mockControl.connector.emit("connect");
assert.deepEqual(received, [["close", "connect"], ["close", "connect"], ["close", "connect"]]);
});
it("should only pass events to subscribers on the same connector", function() {
const otherControl = new MockControl(new MockConnector(
addr({ controlId: 1 }),
addr("controller"),
));
const other = new lib.EventSubscriber(RegisteredEvent, otherControl);
const received = [];
eventSubscriber.handleConnectionEvent = event => { received.push(["first", event]); };
other.handleConnectionEvent = event => { received.push(["other", event]); };
otherControl.connector.emit("close");
assert.deepEqual(received, [["other", "close"]]);
});
});
describe("subscribe()", function() {