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() {