From 4d6b9b52e35282c31dddcc427025cc8c556a6f6b Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 31 Oct 2026 23:07:34 +0100 Subject: [PATCH 1/2] 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() { From f86ebb6e66eb1e10d7a3a272c2c5044dbda34f0b Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Wed, 11 Nov 2026 11:22:08 +0100 Subject: [PATCH 2/2] Keep the connector subscribers as a static on EventSubscriber --- packages/lib/src/subscriptions.ts | 65 ++++++++++++++++--------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index e8595fa9..71ba825e 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -455,41 +455,44 @@ 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 */ export class EventSubscriber { + /** Subscribers created on each connector, they share one connect and one close listener on it */ + private static 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 + */ + private static addConnectorSubscriber( + connector: WebSocketBaseConnector, + subscriber: EventSubscriber, + ) { + let subscribers = EventSubscriber.connectorSubscribers.get(connector); + if (!subscribers) { + const created = new Set>(); + EventSubscriber.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); + } + /** The time at which an event was last received, is less than 0 when there have been no events */ lastUpdatedMs = -1; /** True if this subscriber is currently synced with the source */ @@ -513,7 +516,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 - addConnectorSubscriber(control.connector as WebSocketBaseConnector, this); + EventSubscriber.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") {