From 4d6b9b52e35282c31dddcc427025cc8c556a6f6b Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 31 Oct 2026 23:07:34 +0100 Subject: [PATCH] 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. --- packages/lib/src/subscriptions.ts | 38 ++++++++++++++++++++++------ test/lib/subscriptions.test.js | 41 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index cc66781e..e8595fa9 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -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>>(); + +/** + * 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) { + let subscribers = connectorSubscribers.get(connector); + if (!subscribers) { + const created = new Set>(); + 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 { ) { 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") { diff --git a/test/lib/subscriptions.test.js b/test/lib/subscriptions.test.js index 461ca0ee..45f30078 100644 --- a/test/lib/subscriptions.test.js +++ b/test/lib/subscriptions.test.js @@ -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() {