From 4d6b9b52e35282c31dddcc427025cc8c556a6f6b Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 31 Oct 2026 23:07:34 +0100 Subject: [PATCH 01/11] 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 3868efceadf9303e3d4ee664808106fecd0e89a5 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Wed, 11 Nov 2026 09:38:11 +0000 Subject: [PATCH 02/11] Focus the mod search inputs when they open The "Add mods" card on the mod pack view mounts when shown, so autoFocus is enough there. The Search Mod Portal modal stays mounted after the first open, so it focuses the name input from afterOpenChange instead. Fixes #1194 Co-Authored-By: Claude Opus 5.5 --- packages/web_ui/src/components/ModPackViewPage.tsx | 1 + packages/web_ui/src/components/ModsPage.tsx | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index 3bd8558f..99cad1d0 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -182,6 +182,7 @@ function SearchModsTable(props: SearchModsTableProps) { return <> { diff --git a/packages/web_ui/src/components/ModsPage.tsx b/packages/web_ui/src/components/ModsPage.tsx index 9f22a1a1..731a90a0 100644 --- a/packages/web_ui/src/components/ModsPage.tsx +++ b/packages/web_ui/src/components/ModsPage.tsx @@ -1,7 +1,8 @@ -import React, { Fragment, useState, useContext, useEffect, useMemo } from "react"; +import React, { Fragment, useState, useContext, useEffect, useMemo, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { - Button, Form, Input, Modal, Popconfirm, Space, Table, Typography, Upload, Alert, Select, Tooltip, notification, + Button, Form, Input, InputRef, Modal, Popconfirm, Space, Table, Typography, Upload, Alert, Select, Tooltip, + notification, } from "antd"; import { ImportOutlined, PlusOutlined, SearchOutlined, DownloadOutlined, @@ -147,6 +148,7 @@ function SearchModsButton() { const account = useAccount(); const [open, setOpen] = useState(false); const [form] = Form.useForm(); + const nameInputRef = useRef(null); const [searchText, setSearchText] = useState(""); const [factorioVersion, setFactorioVersion] = useState("2.0"); @@ -333,6 +335,7 @@ function SearchModsButton() { title="Search Mod Portal" open={open} onCancel={() => { setOpen(false); }} + afterOpenChange={visible => { if (visible) { nameInputRef.current?.focus(); } }} width={1000} footer={[