From 75be8e3df82768282365b2b7bc935a9fbc500f58 Mon Sep 17 00:00:00 2001 From: popcorn dan <6363017+popcornhax@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:42:23 -0600 Subject: [PATCH 1/2] getGlobalSettings accumulates orphaned listeners under concurrent or interleaved calls getGlobalSettings() currently uses connection.once() with no id filtering and no timeout. If a didReceiveGlobalSettings event arrives from an unrelated trigger (e.g., the property inspector) while a getGlobalSettings() promise is pending, the wrong event resolves the promise and the listener registered for the actual response is permanently orphaned. Under repeated calls this silently accumulates listeners. This PR aligns getGlobalSettings() with the pattern already used by Action#fetch(): match by request id, use connection.disposableOn() for safe cleanup, and add a timeout to prevent indefinite awaiting. The DidReceiveGlobalSettings type already carries an optional id field for this purpose. --- src/plugin/settings.ts | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/plugin/settings.ts b/src/plugin/settings.ts index 1fbd45f..1a2549d 100644 --- a/src/plugin/settings.ts +++ b/src/plugin/settings.ts @@ -1,4 +1,4 @@ -import type { IDisposable, JsonObject } from "@elgato/utils"; +import type { IDisposable, JsonObject, withResolvers } from "@elgato/utils"; import { randomUUID } from "node:crypto"; import type { DidReceiveGlobalSettings, DidReceiveSettings } from "../api/index.js"; @@ -41,14 +41,29 @@ export const settings = { * @returns Promise containing the plugin's global settings. */ getGlobalSettings: (): Promise => { - return new Promise((resolve) => { - connection.once("didReceiveGlobalSettings", (ev: DidReceiveGlobalSettings) => resolve(ev.payload.settings)); - connection.send({ - event: "getGlobalSettings", - context: connection.registrationParameters.pluginUUID, - id: randomUUID(), - }); - }); + const id = randomUUID(); + const { promise, resolve, reject } = withResolvers(); + + const timeoutId = setTimeout(() => { + listener.dispose(); + reject(new Error("getGlobalSettings timed out")); + }, REQUEST_TIMEOUT); + + const listener = connection.disposableOn("didReceiveGlobalSettings", (ev: DidReceiveGlobalSettings): void => { + if (ev.id === id) { + clearTimeout(timeoutId); + listener.dispose(); + resolve(ev.payload.settings); + } + }); + + connection.send({ + event: "getGlobalSettings", + context: connection.registrationParameters.pluginUUID, + id, + }); + + return promise; }, /** From 2f7f4d22e268dbf4a3fdd3b1bcd05c2a3024db65 Mon Sep 17 00:00:00 2001 From: popcorn dan <6363017+popcornhax@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:49:42 -0600 Subject: [PATCH 2/2] import issue & formatting Fixed import, timeout and formatting. --- src/plugin/settings.ts | 43 ++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/plugin/settings.ts b/src/plugin/settings.ts index 1a2549d..f2163cd 100644 --- a/src/plugin/settings.ts +++ b/src/plugin/settings.ts @@ -1,4 +1,5 @@ -import type { IDisposable, JsonObject, withResolvers } from "@elgato/utils"; +import type { IDisposable, JsonObject } from "@elgato/utils"; +import { withResolvers } from "@elgato/utils"; import { randomUUID } from "node:crypto"; import type { DidReceiveGlobalSettings, DidReceiveSettings } from "../api/index.js"; @@ -8,6 +9,8 @@ import { ActionEvent } from "./events/action-event.js"; import { DidReceiveGlobalSettingsEvent, type DidReceiveSettingsEvent } from "./events/index.js"; import { requiresVersion } from "./validation.js"; +const REQUEST_TIMEOUT = 15 * 1000; // 15s + let __useExperimentalMessageIdentifiers = false; export const settings = { @@ -41,29 +44,29 @@ export const settings = { * @returns Promise containing the plugin's global settings. */ getGlobalSettings: (): Promise => { - const id = randomUUID(); - const { promise, resolve, reject } = withResolvers(); + const id = randomUUID(); + const { promise, resolve, reject } = withResolvers(); - const timeoutId = setTimeout(() => { - listener.dispose(); - reject(new Error("getGlobalSettings timed out")); - }, REQUEST_TIMEOUT); + const timeoutId = setTimeout(() => { + listener.dispose(); + reject(new Error("getGlobalSettings timed out")); + }, REQUEST_TIMEOUT); - const listener = connection.disposableOn("didReceiveGlobalSettings", (ev: DidReceiveGlobalSettings): void => { - if (ev.id === id) { - clearTimeout(timeoutId); - listener.dispose(); - resolve(ev.payload.settings); - } - }); + const listener = connection.disposableOn("didReceiveGlobalSettings", (ev: DidReceiveGlobalSettings): void => { + if (ev.id === id) { + clearTimeout(timeoutId); + listener.dispose(); + resolve(ev.payload.settings); + } + }); - connection.send({ - event: "getGlobalSettings", - context: connection.registrationParameters.pluginUUID, - id, - }); + connection.send({ + event: "getGlobalSettings", + context: connection.registrationParameters.pluginUUID, + id, + }); - return promise; + return promise; }, /**