From d1aa1e76ee85b13621804d08366ac1d324fc68c6 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Mon, 10 Aug 2026 16:53:26 +0100 Subject: [PATCH 01/11] refactor(!): improved type-narrowing when calling setSettings (relates #133) --- .changeset/free-oranges-lose.md | 5 +++ .changeset/wild-bikes-vanish.md | 5 +++ .../plugin/actions/__tests__/service.test.ts | 41 +++++++++---------- packages/plugin/src/plugin/actions/action.ts | 4 +- packages/plugin/src/plugin/actions/dial.ts | 2 +- packages/plugin/src/plugin/actions/key.ts | 2 +- packages/plugin/src/plugin/actions/store.ts | 10 ++--- packages/plugin/src/plugin/devices/device.ts | 4 +- packages/plugin/src/plugin/ui.ts | 6 +-- 9 files changed, 45 insertions(+), 34 deletions(-) create mode 100644 .changeset/free-oranges-lose.md create mode 100644 .changeset/wild-bikes-vanish.md diff --git a/.changeset/free-oranges-lose.md b/.changeset/free-oranges-lose.md new file mode 100644 index 00000000..8b2a26e5 --- /dev/null +++ b/.changeset/free-oranges-lose.md @@ -0,0 +1,5 @@ +--- +"@elgato/streamdeck": major +--- + +`KeyAction` and `DialAction` types now require their setting type be defined. diff --git a/.changeset/wild-bikes-vanish.md b/.changeset/wild-bikes-vanish.md new file mode 100644 index 00000000..b0dc62ab --- /dev/null +++ b/.changeset/wild-bikes-vanish.md @@ -0,0 +1,5 @@ +--- +"@elgato/streamdeck": major +--- + +Fixed type-narrowing when calling `setSettings` on an event's action. diff --git a/packages/plugin/src/plugin/actions/__tests__/service.test.ts b/packages/plugin/src/plugin/actions/__tests__/service.test.ts index dea6c293..65df3e77 100644 --- a/packages/plugin/src/plugin/actions/__tests__/service.test.ts +++ b/packages/plugin/src/plugin/actions/__tests__/service.test.ts @@ -107,7 +107,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialDownEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialDown", }); @@ -153,7 +153,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialRotateEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialRotate", }); @@ -197,7 +197,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialUpEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialUp", }); @@ -242,7 +242,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[KeyDownEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "keyDown", }); @@ -287,7 +287,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[KeyUpEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "keyUp", }); @@ -341,7 +341,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[TitleParametersDidChangeEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "titleParametersDidChange", }); @@ -387,7 +387,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[TouchTapEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "touchTap", }); @@ -432,7 +432,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[WillAppearEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "willAppear", }); @@ -490,7 +490,6 @@ describe("actions", () => { expect(listener).toHaveBeenCalledTimes(1); }); - /** * Asserts settings cache lifecycle updates for appear, settings updates, and disappear events. */ @@ -668,7 +667,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialDownEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialDown", }); @@ -712,7 +711,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialRotateEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialRotate", }); @@ -754,7 +753,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialUpEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialUp", }); @@ -787,7 +786,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[SendToPluginEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: { name: "Hello world", }, @@ -832,7 +831,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DidReceiveSettingsEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "didReceiveSettings", }); @@ -875,7 +874,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[KeyDownEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "keyDown", }); @@ -918,7 +917,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[KeyUpEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "keyUp", }); @@ -949,7 +948,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[PropertyInspectorDidAppearEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, type: "propertyInspectorDidAppear", }); }); @@ -979,7 +978,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[PropertyInspectorDidDisappearEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, type: "propertyInspectorDidDisappear", }); }); @@ -1030,7 +1029,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[TitleParametersDidChangeEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "titleParametersDidChange", }); @@ -1074,7 +1073,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[TouchTapEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "touchTap", }); @@ -1117,7 +1116,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[WillAppearEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "willAppear", }); diff --git a/packages/plugin/src/plugin/actions/action.ts b/packages/plugin/src/plugin/actions/action.ts index 53596836..4cc8ad0b 100644 --- a/packages/plugin/src/plugin/actions/action.ts +++ b/packages/plugin/src/plugin/actions/action.ts @@ -24,7 +24,7 @@ const REQUEST_TIMEOUT = 15 * 1000; // 15s * Provides a contextualized instance of an {@link Action}, allowing for direct communication with the Stream Deck. * @template T The type of settings associated with the action. */ -export class Action extends ActionContext { +export class Action extends ActionContext { /** * Gets the resources (files) associated with this action; these resources are embedded into the * action when it is exported, either individually, or as part of a profile. @@ -108,7 +108,7 @@ export class Action extends ActionContext { * @param value Settings to persist. * @returns `Promise` resolved when the settings are sent to Stream Deck. */ - public setSettings(value: U): Promise { + public setSettings(value: T): Promise { settingsCache.delete(this.id); return connection.send({ event: "setSettings", diff --git a/packages/plugin/src/plugin/actions/dial.ts b/packages/plugin/src/plugin/actions/dial.ts index 34b534f4..db70ae94 100644 --- a/packages/plugin/src/plugin/actions/dial.ts +++ b/packages/plugin/src/plugin/actions/dial.ts @@ -8,7 +8,7 @@ import { Action } from "./action.js"; * Provides a contextualized instance of a dial action. * @template T The type of settings associated with the action. */ -export class DialAction extends Action { +export class DialAction extends Action { /** * Private backing field for {@link DialAction.coordinates}. */ diff --git a/packages/plugin/src/plugin/actions/key.ts b/packages/plugin/src/plugin/actions/key.ts index 7d28fa2d..76d08177 100644 --- a/packages/plugin/src/plugin/actions/key.ts +++ b/packages/plugin/src/plugin/actions/key.ts @@ -8,7 +8,7 @@ import { Action } from "./action.js"; * Provides a contextualized instance of a key action. * @template T The type of settings associated with the action. */ -export class KeyAction extends Action { +export class KeyAction extends Action { /** * Private backing field for {@link KeyAction.coordinates}. */ diff --git a/packages/plugin/src/plugin/actions/store.ts b/packages/plugin/src/plugin/actions/store.ts index 409245eb..19e7d709 100644 --- a/packages/plugin/src/plugin/actions/store.ts +++ b/packages/plugin/src/plugin/actions/store.ts @@ -1,14 +1,14 @@ -import { Enumerable } from "@elgato/utils"; +import { Enumerable, type JsonObject } from "@elgato/utils"; import type { DialAction } from "./dial.js"; import type { KeyAction } from "./key.js"; -const __items = new Map(); +const __items = new Map | KeyAction>(); /** * Provides a read-only store of Stream Deck devices. */ -export class ReadOnlyActionStore extends Enumerable { +export class ReadOnlyActionStore extends Enumerable | KeyAction> { /** * Initializes a new instance of the {@link ReadOnlyActionStore}. */ @@ -21,7 +21,7 @@ export class ReadOnlyActionStore extends Enumerable { * @param id Identifier of action to search for. * @returns The action, when present; otherwise `undefined`. */ - public getActionById(id: string): DialAction | KeyAction | undefined { + public getActionById(id: string): DialAction | KeyAction | undefined { return __items.get(id); } } @@ -42,7 +42,7 @@ class ActionStore extends ReadOnlyActionStore { * Adds the action to the store. * @param action The action. */ - public set(action: DialAction | KeyAction): void { + public set(action: DialAction | KeyAction): void { __items.set(action.id, action); } } diff --git a/packages/plugin/src/plugin/devices/device.ts b/packages/plugin/src/plugin/devices/device.ts index 568b959f..4dfb23f3 100644 --- a/packages/plugin/src/plugin/devices/device.ts +++ b/packages/plugin/src/plugin/devices/device.ts @@ -1,3 +1,5 @@ +import type { JsonObject } from "@elgato/utils"; + import type { DeviceInfo, DeviceType, Size } from "../../api/index.js"; import type { DialAction } from "../actions/dial.js"; import type { KeyAction } from "../actions/key.js"; @@ -61,7 +63,7 @@ export class Device { * Actions currently visible on the device. * @returns Collection of visible actions. */ - public get actions(): IterableIterator { + public get actions(): IterableIterator | KeyAction> { return actionStore.filter((a) => a.device.id === this.id); } diff --git a/packages/plugin/src/plugin/ui.ts b/packages/plugin/src/plugin/ui.ts index 4b888149..1c4781ab 100644 --- a/packages/plugin/src/plugin/ui.ts +++ b/packages/plugin/src/plugin/ui.ts @@ -18,7 +18,7 @@ class UIController { /** * Action associated with the current property inspector. */ - #action: DialAction | KeyAction | undefined; + #action: DialAction | KeyAction | undefined; /** * To overcome event races, the debounce counter keeps track of appear vs disappear events, ensuring @@ -54,7 +54,7 @@ class UIController { * Gets the action associated with the current property. * @returns The action; otherwise `undefined` when a property inspector is not visible. */ - public get action(): DialAction | KeyAction | undefined { + public get action(): DialAction | KeyAction | undefined { return this.#action; } @@ -134,7 +134,7 @@ class UIController { * @param action Action to check against. * @returns `true` when the actions are the same. */ - #isCurrent(action: DialAction | KeyAction): boolean { + #isCurrent(action: DialAction | KeyAction): boolean { return ( this.#action?.id === action.id && this.#action?.manifestId === action.manifestId && From 79232a0a11b0e5b55529ae20f22f972c46e20ba1 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Tue, 11 Aug 2026 15:05:51 +0100 Subject: [PATCH 02/11] chore: address feedback --- .changeset/free-oranges-lose.md | 2 +- packages/plugin/src/plugin/actions/store.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/free-oranges-lose.md b/.changeset/free-oranges-lose.md index 8b2a26e5..cfb1b557 100644 --- a/.changeset/free-oranges-lose.md +++ b/.changeset/free-oranges-lose.md @@ -2,4 +2,4 @@ "@elgato/streamdeck": major --- -`KeyAction` and `DialAction` types now require their setting type be defined. +`Action`, `KeyAction`, and `DialAction` types now require their setting type be defined. diff --git a/packages/plugin/src/plugin/actions/store.ts b/packages/plugin/src/plugin/actions/store.ts index 19e7d709..cb18dd0f 100644 --- a/packages/plugin/src/plugin/actions/store.ts +++ b/packages/plugin/src/plugin/actions/store.ts @@ -6,7 +6,7 @@ import type { KeyAction } from "./key.js"; const __items = new Map | KeyAction>(); /** - * Provides a read-only store of Stream Deck devices. + * Provides a read-only store of Stream Deck actions. */ export class ReadOnlyActionStore extends Enumerable | KeyAction> { /** From 92fad4a7ca52eed834a23514953dbf1eadde9512 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Thu, 20 Aug 2026 13:58:12 +0100 Subject: [PATCH 03/11] test: fix types --- packages/plugin/src/plugin/actions/__tests__/dial.test.ts | 6 +++--- packages/plugin/src/plugin/actions/__tests__/key.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/plugin/src/plugin/actions/__tests__/dial.test.ts b/packages/plugin/src/plugin/actions/__tests__/dial.test.ts index a65258e2..e8503191 100644 --- a/packages/plugin/src/plugin/actions/__tests__/dial.test.ts +++ b/packages/plugin/src/plugin/actions/__tests__/dial.test.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from "@elgato/utils"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { @@ -8,7 +9,6 @@ import { type SetTriggerDescription, type WillAppear, } from "../../../api/index.js"; -import type { JsonObject } from "../../../common/json.js"; import { connection } from "../../connection.js"; import { Device } from "../../devices/device.js"; import { deviceStore } from "../../devices/store.js"; @@ -115,7 +115,7 @@ describe("DialAction", () => { // Act. const jsonStr = JSON.stringify(action); - const jsonObj: DialAction = JSON.parse(jsonStr); + const jsonObj: DialAction = JSON.parse(jsonStr); // Assert. expect(jsonObj.controllerType).toBe(action.controllerType); @@ -126,7 +126,7 @@ describe("DialAction", () => { }); describe("sending", () => { - let action!: DialAction; + let action!: DialAction; beforeAll(() => (action = new DialAction(source))); /** diff --git a/packages/plugin/src/plugin/actions/__tests__/key.test.ts b/packages/plugin/src/plugin/actions/__tests__/key.test.ts index b62e66dd..5a587a86 100644 --- a/packages/plugin/src/plugin/actions/__tests__/key.test.ts +++ b/packages/plugin/src/plugin/actions/__tests__/key.test.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from "@elgato/utils"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { @@ -9,7 +10,6 @@ import { Target, type WillAppear, } from "../../../api/index.js"; -import type { JsonObject } from "../../../common/json.js"; import { connection } from "../../connection.js"; import { Device } from "../../devices/device.js"; import { deviceStore } from "../../devices/store.js"; @@ -147,7 +147,7 @@ describe("KeyAction", () => { // Act. const jsonStr = JSON.stringify(action); - const jsonObj: KeyAction = JSON.parse(jsonStr); + const jsonObj: KeyAction = JSON.parse(jsonStr); // Assert. expect(jsonObj.controllerType).toBe(action.controllerType); @@ -159,7 +159,7 @@ describe("KeyAction", () => { }); describe("sending", () => { - let action!: KeyAction; + let action!: KeyAction; beforeAll(() => (action = new KeyAction(source))); /** From b5140abb41b64e70ee4251faeb7c179c4bb68277 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Thu, 20 Aug 2026 14:42:09 +0100 Subject: [PATCH 04/11] refactor(!): improve type-narrowing of getSettings in favour of casting, improve JSDocs. --- packages/plugin/src/plugin/actions/action.ts | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/plugin/src/plugin/actions/action.ts b/packages/plugin/src/plugin/actions/action.ts index 4cc8ad0b..d2bb33c6 100644 --- a/packages/plugin/src/plugin/actions/action.ts +++ b/packages/plugin/src/plugin/actions/action.ts @@ -21,13 +21,13 @@ import type { KeyAction } from "./key.js"; const REQUEST_TIMEOUT = 15 * 1000; // 15s /** - * Provides a contextualized instance of an {@link Action}, allowing for direct communication with the Stream Deck. - * @template T The type of settings associated with the action. + * Provides a contextualized instance of an action, allowing for direct communication with the Stream Deck. + * @template TSettings The type of settings associated with the action. */ -export class Action extends ActionContext { +export class Action extends ActionContext { /** - * Gets the resources (files) associated with this action; these resources are embedded into the - * action when it is exported, either individually, or as part of a profile. + * Gets the resources (files) associated with this action; these resources are embedded into the action when it is + * exported, either individually, or as part of a profile. * * Available from Stream Deck 7.1. * @returns The resources. @@ -44,7 +44,7 @@ export class Action extends ActionContext { * @template U The type of settings associated with the action.D * @returns Promise containing the action instance's settings. */ - public async getSettings(): Promise { + public async getSettings(): Promise { if (actionConfig.useExperimentalMessageIdentifiers) { const cached = settingsCache.get(this.id); if (cached !== undefined) { @@ -56,19 +56,19 @@ export class Action extends ActionContext { settings: cached, }), ); - return cached as U; + return cached as TSettings; } } const res = await this.#fetch("getSettings", "didReceiveSettings"); - return res.payload.settings as U; + return res.payload.settings as TSettings; } /** * Determines whether this instance is a dial. * @returns `true` when this instance is a dial; otherwise `false`. */ - public isDial(): this is DialAction { + public isDial(): this is DialAction { return this.controllerType === "Encoder"; } @@ -76,13 +76,13 @@ export class Action extends ActionContext { * Determines whether this instance is a key. * @returns `true` when this instance is a key; otherwise `false`. */ - public isKey(): this is KeyAction { + public isKey(): this is KeyAction { return this.controllerType === "Keypad"; } /** - * Sets the resources (files) associated with this action; these resources are embedded into the - * action when it is exported, either individually, or as part of a profile. + * Sets the resources (files) associated with this action; these resources are embedded into the action when it is + * exported, either individually, or as part of a profile. * * Available from Stream Deck 7.1. * @example @@ -104,11 +104,11 @@ export class Action extends ActionContext { } /** - * Sets the settings associated with this action instance. Use in conjunction with {@link Action.getSettings}. + * Sets the settings associated with this action instance. * @param value Settings to persist. * @returns `Promise` resolved when the settings are sent to Stream Deck. */ - public setSettings(value: T): Promise { + public setSettings(value: TSettings): Promise { settingsCache.delete(this.id); return connection.send({ event: "setSettings", @@ -118,11 +118,11 @@ export class Action extends ActionContext { } /** - * Temporarily shows an alert (i.e. warning), in the form of an exclamation mark in a yellow triangle, on this action instance. Used to provide visual feedback when an action failed. - * @returns `Promise` resolved when the request to show an alert has been sent to Stream Deck. + * Temporarily shows an alert (i.e. warning), in the form of an exclamation mark in a yellow triangle, on this + * action instance. Used to provide visual feedback when an action failed. */ - public showAlert(): Promise { - return connection.send({ + public async showAlert(): Promise { + await connection.send({ event: "showAlert", context: this.id, }); From c6f162181f977e94065a02bd70d0f6f5a27e2783 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Thu, 20 Aug 2026 15:36:23 +0100 Subject: [PATCH 05/11] refactor: remove default settings type, and improve JSDocs --- packages/plugin/src/plugin/actions/dial.ts | 6 +++--- packages/plugin/src/plugin/actions/key.ts | 6 +++--- packages/plugin/src/plugin/actions/neo-infobar.ts | 2 +- packages/plugin/src/plugin/actions/service.ts | 4 +++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/plugin/src/plugin/actions/dial.ts b/packages/plugin/src/plugin/actions/dial.ts index a06973fa..29cb89c2 100644 --- a/packages/plugin/src/plugin/actions/dial.ts +++ b/packages/plugin/src/plugin/actions/dial.ts @@ -6,11 +6,11 @@ import { Action } from "./action.js"; /** * Provides a contextualized instance of a dial action. - * @template T The type of settings associated with the action. + * @template TSettings The type of settings associated with the action. */ -export class DialAction extends Action { +export class DialAction extends Action { /** - * Private backing field for {@link DialAction.coordinates}. + * Private backing field for the coordinates. */ readonly #coordinates: Readonly; diff --git a/packages/plugin/src/plugin/actions/key.ts b/packages/plugin/src/plugin/actions/key.ts index a6750a00..dc117435 100644 --- a/packages/plugin/src/plugin/actions/key.ts +++ b/packages/plugin/src/plugin/actions/key.ts @@ -6,11 +6,11 @@ import { Action } from "./action.js"; /** * Provides a contextualized instance of a key action. - * @template T The type of settings associated with the action. + * @template TSettings The type of settings associated with the action. */ -export class KeyAction extends Action { +export class KeyAction extends Action { /** - * Private backing field for {@link KeyAction.coordinates}. + * Private backing field for the coordinates. */ readonly #coordinates: Readonly | undefined; diff --git a/packages/plugin/src/plugin/actions/neo-infobar.ts b/packages/plugin/src/plugin/actions/neo-infobar.ts index 2678251d..c244613c 100644 --- a/packages/plugin/src/plugin/actions/neo-infobar.ts +++ b/packages/plugin/src/plugin/actions/neo-infobar.ts @@ -8,7 +8,7 @@ import { Action } from "./action.js"; * Provides a contextualized instance of an infobar action found on Stream Deck Neo. * @template T The type of settings associated with the action. */ -export class NeoInfobarAction extends Action { +export class NeoInfobarAction extends Action { /** * Private backing field for the coordinates. */ diff --git a/packages/plugin/src/plugin/actions/service.ts b/packages/plugin/src/plugin/actions/service.ts index 0ffa530d..5b5aee34 100644 --- a/packages/plugin/src/plugin/actions/service.ts +++ b/packages/plugin/src/plugin/actions/service.ts @@ -299,7 +299,9 @@ class ActionService extends ReadOnlyActionStore { * @param ev Event that contains the controller. * @returns The action instance. */ - #createAction(ev: WillAppear): DialAction | KeyAction | NeoInfobarAction { + #createAction( + ev: WillAppear, + ): DialAction | KeyAction | NeoInfobarAction { switch (ev.payload.controller) { case "Encoder": return new DialAction(ev); From 4dd7c7404d9912fbcfb7e21843a14914f308a0eb Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Thu, 20 Aug 2026 16:59:01 +0100 Subject: [PATCH 06/11] refactor(!): further type-narrowing of settings --- .changeset/pink-jars-admire.md | 5 + .changeset/some-kings-taste.md | 5 + .gitignore | 3 + .../plugin/actions/__tests__/action.test.ts | 30 +-- .../src/plugin/actions/__tests__/dial.test.ts | 4 +- .../src/plugin/actions/__tests__/key.test.ts | 4 +- .../plugin/src/plugin/actions/action-base.ts | 176 ++++++++++++++++++ packages/plugin/src/plugin/actions/action.ts | 175 +---------------- packages/plugin/src/plugin/actions/dial.ts | 8 +- packages/plugin/src/plugin/actions/key.ts | 4 +- .../plugin/src/plugin/actions/neo-infobar.ts | 4 +- packages/plugin/src/plugin/actions/service.ts | 31 ++- .../src/plugin/actions/singleton-action.ts | 16 +- packages/plugin/src/plugin/actions/store.ts | 18 +- packages/plugin/src/plugin/settings.ts | 2 +- packages/plugin/src/plugin/ui.ts | 6 +- 16 files changed, 254 insertions(+), 237 deletions(-) create mode 100644 .changeset/pink-jars-admire.md create mode 100644 .changeset/some-kings-taste.md create mode 100644 packages/plugin/src/plugin/actions/action-base.ts diff --git a/.changeset/pink-jars-admire.md b/.changeset/pink-jars-admire.md new file mode 100644 index 00000000..cb5b87a0 --- /dev/null +++ b/.changeset/pink-jars-admire.md @@ -0,0 +1,5 @@ +--- +"@elgato/streamdeck": major +--- + +Updated the settings type used by `getSettings` and `setSettings` to use the type defined on the action. diff --git a/.changeset/some-kings-taste.md b/.changeset/some-kings-taste.md new file mode 100644 index 00000000..452d5f25 --- /dev/null +++ b/.changeset/some-kings-taste.md @@ -0,0 +1,5 @@ +--- +"@elgato/streamdeck": major +--- + +Updated `Action` type to represent a union of possible action types, such as key, dial, or Neo infobar. diff --git a/.gitignore b/.gitignore index 6e88c1f6..7e53fa2d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ dist/ bin/ .wireit/ +# Currently reserved for local testing +apps/ + # Shared package files /packages/*/LICENSE /packages/plugin/README.md diff --git a/packages/plugin/src/plugin/actions/__tests__/action.test.ts b/packages/plugin/src/plugin/actions/__tests__/action.test.ts index 9e4b7290..d26070ca 100644 --- a/packages/plugin/src/plugin/actions/__tests__/action.test.ts +++ b/packages/plugin/src/plugin/actions/__tests__/action.test.ts @@ -7,7 +7,7 @@ import { connection } from "../../connection.js"; import { Device } from "../../devices/device.js"; import { deviceStore } from "../../devices/store.js"; import { logger } from "../../logging/index.js"; -import { Action } from "../action.js"; +import { ActionBase } from "../action-base.js"; import { settingsCache } from "../cache.js"; import { actionConfig } from "../config.js"; import { DialAction } from "../dial.js"; @@ -59,14 +59,14 @@ describe("Action", () => { }); /** - * Asserts the constructor of {@link Action} sets the properties from the source. + * Asserts the constructor of {@link ActionBase} sets the properties from the source. */ it("constructor sets properties from source", () => { // Arrange, act. - const action = new Action(source); + const action = new ActionBase(source); // Assert. - expect(action).toBeInstanceOf(Action); + expect(action).toBeInstanceOf(ActionBase); expect(action.controllerType).toBe("Keypad"); expect(action.device).toBe(device); expect(action.id).toBe(source.context); @@ -85,11 +85,11 @@ describe("Action", () => { }); /** - * Asserts {@link Action.getSettings} returns cached settings when the cache is valid. + * Asserts {@link ActionBase.getSettings} returns cached settings when the cache is valid. */ it("getSettings returns cached settings", async () => { // Arrange. - const action = new Action(source); + const action = new ActionBase(source); const cachedSettings = { name: "Cached" }; const spyOnTrace = vi.spyOn(logger, "trace"); settingsCache.set(action.id, cachedSettings); @@ -118,11 +118,11 @@ describe("Action", () => { }); /** - * Asserts {@link Action.getSettings} ignores cached settings when experimental message identifiers are disabled. + * Asserts {@link ActionBase.getSettings} ignores cached settings when experimental message identifiers are disabled. */ it("getSettings ignores cached settings", async () => { // Arrange. - const action = new Action(source); + const action = new ActionBase(source); const spyOnTrace = vi.spyOn(logger, "trace"); settingsCache.set(action.id, { name: "Cached" }); @@ -164,11 +164,11 @@ describe("Action", () => { }); /** - * Asserts {@link Action.getSettings} requests settings from the connection and does not populate cache. + * Asserts {@link ActionBase.getSettings} requests settings from the connection and does not populate cache. */ it("getSettings fetches without populating cache", async () => { // Arrange. - const action = new Action(source); + const action = new ActionBase(source); // Array, act (Command). const settings = action.getSettings(); @@ -262,7 +262,7 @@ describe("Action", () => { * Asserts type-checking when the controller is "Keypad". */ test("keypad type assertion", () => { - const action = new Action({ + const action = new ActionBase({ ...source, payload: { ...source.payload, @@ -295,11 +295,11 @@ describe("Action", () => { beforeAll(() => (action = new KeyAction(source))); /** - * Asserts {@link Action.setSettings} invalidates the settings cache. + * Asserts {@link ActionBase.setSettings} invalidates the settings cache. */ it("setSettings invalidates cache", async () => { // Arrange. - const action = new Action(source); + const action = new ActionBase(source); settingsCache.set(action.id, { name: "Cached" }); // Act. @@ -313,7 +313,7 @@ describe("Action", () => { }); /** - * Asserts {@link Action.setSettings} forwards the command to the {@link connection}. + * Asserts {@link ActionBase.setSettings} forwards the command to the {@link connection}. */ it("setSettings", async () => { // Arrange, act. @@ -333,7 +333,7 @@ describe("Action", () => { }); /** - * Asserts {@link Action.showAlert} forwards the command to the {@link connection}. + * Asserts {@link ActionBase.showAlert} forwards the command to the {@link connection}. */ it("showAlert", async () => { // Arrange, act. diff --git a/packages/plugin/src/plugin/actions/__tests__/dial.test.ts b/packages/plugin/src/plugin/actions/__tests__/dial.test.ts index e8503191..4b646128 100644 --- a/packages/plugin/src/plugin/actions/__tests__/dial.test.ts +++ b/packages/plugin/src/plugin/actions/__tests__/dial.test.ts @@ -12,7 +12,7 @@ import { import { connection } from "../../connection.js"; import { Device } from "../../devices/device.js"; import { deviceStore } from "../../devices/store.js"; -import { Action } from "../action.js"; +import { ActionBase } from "../action-base.js"; import { DialAction } from "../dial.js"; vi.mock("../../devices/store.js"); @@ -63,7 +63,7 @@ describe("DialAction", () => { const action = new DialAction(source); // Assert. - expect(action).toBeInstanceOf(Action); + expect(action).toBeInstanceOf(ActionBase); expect(action.coordinates).not.toBeUndefined(); expect(action.coordinates?.column).toBe(1); expect(action.coordinates?.row).toBe(2); diff --git a/packages/plugin/src/plugin/actions/__tests__/key.test.ts b/packages/plugin/src/plugin/actions/__tests__/key.test.ts index 5a587a86..5cb2eef1 100644 --- a/packages/plugin/src/plugin/actions/__tests__/key.test.ts +++ b/packages/plugin/src/plugin/actions/__tests__/key.test.ts @@ -13,7 +13,7 @@ import { import { connection } from "../../connection.js"; import { Device } from "../../devices/device.js"; import { deviceStore } from "../../devices/store.js"; -import { Action } from "../action.js"; +import { ActionBase } from "../action-base.js"; import { KeyAction } from "../key.js"; vi.mock("../../devices/store.js"); @@ -64,7 +64,7 @@ describe("KeyAction", () => { const action = new KeyAction(source); // Assert. - expect(action).toBeInstanceOf(Action); + expect(action).toBeInstanceOf(ActionBase); expect(action.coordinates).not.toBeUndefined(); expect(action.coordinates?.column).toBe(1); expect(action.coordinates?.row).toBe(2); diff --git a/packages/plugin/src/plugin/actions/action-base.ts b/packages/plugin/src/plugin/actions/action-base.ts new file mode 100644 index 00000000..2bfc28b5 --- /dev/null +++ b/packages/plugin/src/plugin/actions/action-base.ts @@ -0,0 +1,176 @@ +import { type EventArgs, type JsonObject, withResolvers } from "@elgato/utils"; +import { randomUUID } from "node:crypto"; + +import type { + DidReceiveResources, + DidReceiveSettings, + GetResources, + GetSettings, + PluginEventMap, + Resources, +} from "../../api/index.js"; +import { connection } from "../connection.js"; +import { logger } from "../logging/index.js"; +import { requiresVersion } from "../validation.js"; +import { settingsCache } from "./cache.js"; +import { actionConfig } from "./config.js"; +import { ActionContext } from "./context.js"; +import type { DialAction } from "./dial.js"; +import type { KeyAction } from "./key.js"; +import type { NeoInfobarAction } from "./neo-infobar.js"; + +const REQUEST_TIMEOUT = 15 * 1000; // 15s + +/** + * Provides a contextualized instance of an action, allowing for direct communication with the Stream Deck. + * @template TSettings The type of settings associated with the action. + */ +export class ActionBase extends ActionContext { + /** + * Gets the resources (files) associated with this action; these resources are embedded into the action when it is + * exported, either individually, or as part of a profile. + * + * Available from Stream Deck 7.1. + * @returns The resources. + */ + public async getResources(): Promise { + requiresVersion(7.1, connection.version, "getResources"); + + const res = await this.#fetch("getResources", "didReceiveResources"); + return res.payload.resources; + } + + /** + * Gets the settings associated this action instance. + * @template U The type of settings associated with the action.D + * @returns Promise containing the action instance's settings. + */ + public async getSettings(): Promise { + if (actionConfig.useExperimentalMessageIdentifiers) { + const cached = settingsCache.get(this.id); + if (cached !== undefined) { + logger.trace( + JSON.stringify({ + event: "getSettings", + context: this.id, + source: "cache", + settings: cached, + }), + ); + return cached as TSettings; + } + } + + const res = await this.#fetch("getSettings", "didReceiveSettings"); + return res.payload.settings as TSettings; + } + + /** + * Determines whether this instance is a dial. + * @returns `true` when this instance is a dial; otherwise `false`. + */ + public isDial(): this is DialAction { + return this.controllerType === "Encoder"; + } + + /** + * Determines whether this instance is a key. + * @returns `true` when this instance is a key; otherwise `false`. + */ + public isKey(): this is KeyAction { + return this.controllerType === "Keypad"; + } + + /** + * Determines whether this instance is an infobar. + * @returns `true` when this instance is an infobar; otherwise `false`. + */ + public isNeoInfobar(): this is NeoInfobarAction { + return this.controllerType === "Neo"; + } + + /** + * Sets the resources (files) associated with this action; these resources are embedded into the action when it is + * exported, either individually, or as part of a profile. + * + * Available from Stream Deck 7.1. + * @example + * action.setResources({ + * fileOne: "c:\\hello-world.txt", + * anotherFile: "c:\\icon.png" + * }); + * @param resources The resources as a map of file paths. + * @returns `Promise` resolved when the resources are saved to Stream Deck. + */ + public setResources(resources: Resources): Promise { + requiresVersion(7.1, connection.version, "setResources"); + + return connection.send({ + event: "setResources", + context: this.id, + payload: resources, + }); + } + + /** + * Sets the settings associated with this action instance. + * @param value Settings to persist. + * @returns `Promise` resolved when the settings are sent to Stream Deck. + */ + public setSettings(value: TSettings): Promise { + settingsCache.delete(this.id); + return connection.send({ + event: "setSettings", + context: this.id, + payload: value, + }); + } + + /** + * Fetches information from Stream Deck by sending the command, and awaiting the event. + * @param command Name of the event (command) to send. + * @param event Name of the event to await. + * @returns The payload from the received event. + */ + async #fetch( + command: GetterEvent, + event: TEvent, + ): Promise[0]> { + const { resolve, reject, promise } = withResolvers[0]>(); + + // Set a timeout to prevent endless awaiting. + const timeoutId = setTimeout(() => { + listener.dispose(); + reject("The request timed out"); + }, REQUEST_TIMEOUT); + + // Listen for an event that can resolve the request. + const listener = connection.disposableOn(event, (ev): void => { + // Make sure the received event is for this action. + if (ev.context == this.id) { + clearTimeout(timeoutId); + listener.dispose(); + resolve(ev); + } + }); + + // Send the request; specifying an id signifies its a request. + await connection.send({ + event: command, + context: this.id, + id: randomUUID(), + }); + + return promise; + } +} + +/** + * Events that represents a request for information from Stream Deck. + */ +type GetterEvent = (GetResources | GetSettings)["event"]; + +/** + * Events that represents a response of information from Stream Deck. + */ +type DidReceiveEvent = (DidReceiveResources | DidReceiveSettings)["event"]; diff --git a/packages/plugin/src/plugin/actions/action.ts b/packages/plugin/src/plugin/actions/action.ts index fcd1fe5e..ad8e28ec 100644 --- a/packages/plugin/src/plugin/actions/action.ts +++ b/packages/plugin/src/plugin/actions/action.ts @@ -1,176 +1,13 @@ -import { type EventArgs, type JsonObject, withResolvers } from "@elgato/utils"; -import { randomUUID } from "node:crypto"; +import type { JsonObject } from "@elgato/utils"; -import type { - DidReceiveResources, - DidReceiveSettings, - GetResources, - GetSettings, - PluginEventMap, - Resources, -} from "../../api/index.js"; -import { connection } from "../connection.js"; -import { logger } from "../logging/index.js"; -import { requiresVersion } from "../validation.js"; -import { settingsCache } from "./cache.js"; -import { actionConfig } from "./config.js"; -import { ActionContext } from "./context.js"; import type { DialAction } from "./dial.js"; import type { KeyAction } from "./key.js"; import type { NeoInfobarAction } from "./neo-infobar.js"; -const REQUEST_TIMEOUT = 15 * 1000; // 15s - -/** - * Provides a contextualized instance of an action, allowing for direct communication with the Stream Deck. - * @template TSettings The type of settings associated with the action. - */ -export class Action extends ActionContext { - /** - * Gets the resources (files) associated with this action; these resources are embedded into the action when it is - * exported, either individually, or as part of a profile. - * - * Available from Stream Deck 7.1. - * @returns The resources. - */ - public async getResources(): Promise { - requiresVersion(7.1, connection.version, "getResources"); - - const res = await this.#fetch("getResources", "didReceiveResources"); - return res.payload.resources; - } - - /** - * Gets the settings associated this action instance. - * @template U The type of settings associated with the action.D - * @returns Promise containing the action instance's settings. - */ - public async getSettings(): Promise { - if (actionConfig.useExperimentalMessageIdentifiers) { - const cached = settingsCache.get(this.id); - if (cached !== undefined) { - logger.trace( - JSON.stringify({ - event: "getSettings", - context: this.id, - source: "cache", - settings: cached, - }), - ); - return cached as TSettings; - } - } - - const res = await this.#fetch("getSettings", "didReceiveSettings"); - return res.payload.settings as TSettings; - } - - /** - * Determines whether this instance is a dial. - * @returns `true` when this instance is a dial; otherwise `false`. - */ - public isDial(): this is DialAction { - return this.controllerType === "Encoder"; - } - - /** - * Determines whether this instance is a key. - * @returns `true` when this instance is a key; otherwise `false`. - */ - public isKey(): this is KeyAction { - return this.controllerType === "Keypad"; - } - - /** - * Determines whether this instance is an infobar. - * @returns `true` when this instance is an infobar; otherwise `false`. - */ - public isNeoInfobar(): this is NeoInfobarAction { - return this.controllerType === "Neo"; - } - - /** - * Sets the resources (files) associated with this action; these resources are embedded into the action when it is - * exported, either individually, or as part of a profile. - * - * Available from Stream Deck 7.1. - * @example - * action.setResources({ - * fileOne: "c:\\hello-world.txt", - * anotherFile: "c:\\icon.png" - * }); - * @param resources The resources as a map of file paths. - * @returns `Promise` resolved when the resources are saved to Stream Deck. - */ - public setResources(resources: Resources): Promise { - requiresVersion(7.1, connection.version, "setResources"); - - return connection.send({ - event: "setResources", - context: this.id, - payload: resources, - }); - } - - /** - * Sets the settings associated with this action instance. - * @param value Settings to persist. - * @returns `Promise` resolved when the settings are sent to Stream Deck. - */ - public setSettings(value: TSettings): Promise { - settingsCache.delete(this.id); - return connection.send({ - event: "setSettings", - context: this.id, - payload: value, - }); - } - - /** - * Fetches information from Stream Deck by sending the command, and awaiting the event. - * @param command Name of the event (command) to send. - * @param event Name of the event to await. - * @returns The payload from the received event. - */ - async #fetch( - command: GetterEvent, - event: TEvent, - ): Promise[0]> { - const { resolve, reject, promise } = withResolvers[0]>(); - - // Set a timeout to prevent endless awaiting. - const timeoutId = setTimeout(() => { - listener.dispose(); - reject("The request timed out"); - }, REQUEST_TIMEOUT); - - // Listen for an event that can resolve the request. - const listener = connection.disposableOn(event, (ev): void => { - // Make sure the received event is for this action. - if (ev.context == this.id) { - clearTimeout(timeoutId); - listener.dispose(); - resolve(ev); - } - }); - - // Send the request; specifying an id signifies its a request. - await connection.send({ - event: command, - context: this.id, - id: randomUUID(), - }); - - return promise; - } -} - -/** - * Events that represents a request for information from Stream Deck. - */ -type GetterEvent = (GetResources | GetSettings)["event"]; - /** - * Events that represents a response of information from Stream Deck. + * Union of available action types. */ -type DidReceiveEvent = (DidReceiveResources | DidReceiveSettings)["event"]; +export type Action = + | DialAction + | KeyAction + | NeoInfobarAction; diff --git a/packages/plugin/src/plugin/actions/dial.ts b/packages/plugin/src/plugin/actions/dial.ts index 29cb89c2..76acb20c 100644 --- a/packages/plugin/src/plugin/actions/dial.ts +++ b/packages/plugin/src/plugin/actions/dial.ts @@ -2,13 +2,13 @@ import type { JsonObject } from "@elgato/utils"; import type { Coordinates, FeedbackPayload, SetTriggerDescription, WillAppear } from "../../api/index.js"; import { connection } from "../connection.js"; -import { Action } from "./action.js"; +import { ActionBase } from "./action-base.js"; /** * Provides a contextualized instance of a dial action. * @template TSettings The type of settings associated with the action. */ -export class DialAction extends Action { +export class DialAction extends ActionBase { /** * Private backing field for the coordinates. */ @@ -38,7 +38,7 @@ export class DialAction extends Action /** * Sets the feedback for the current layout associated with this action instance, allowing for the visual items to be updated. Layouts are a powerful way to provide dynamic information - * to users, and can be assigned in the manifest, or dynamically via {@link Action.setFeedbackLayout}. + * to users, and can be assigned in the manifest, or dynamically via {@link ActionBase.setFeedbackLayout}. * * The {@link feedback} payload defines which items within the layout will be updated, and are identified by their property name (defined as the `key` in the layout's definition). * The values can either by a complete new definition, a `string` for layout item types of `text` and `pixmap`, or a `number` for layout item types of `bar` and `gbar`. @@ -55,7 +55,7 @@ export class DialAction extends Action /** * Sets the layout associated with this action instance. The layout must be either a built-in layout identifier, or path to a local layout JSON file within the plugin's folder. - * Use in conjunction with {@link Action.setFeedback} to update the layout's current items' settings. + * Use in conjunction with {@link ActionBase.setFeedback} to update the layout's current items' settings. * @param layout Name of a pre-defined layout, or relative path to a custom one. * @returns `Promise` resolved when the new layout has been sent to Stream Deck. */ diff --git a/packages/plugin/src/plugin/actions/key.ts b/packages/plugin/src/plugin/actions/key.ts index dc117435..77730020 100644 --- a/packages/plugin/src/plugin/actions/key.ts +++ b/packages/plugin/src/plugin/actions/key.ts @@ -2,13 +2,13 @@ import type { JsonObject } from "@elgato/utils"; import type { Coordinates, SetImage, SetTitle, State, WillAppear } from "../../api/index.js"; import { connection } from "../connection.js"; -import { Action } from "./action.js"; +import { ActionBase } from "./action-base.js"; /** * Provides a contextualized instance of a key action. * @template TSettings The type of settings associated with the action. */ -export class KeyAction extends Action { +export class KeyAction extends ActionBase { /** * Private backing field for the coordinates. */ diff --git a/packages/plugin/src/plugin/actions/neo-infobar.ts b/packages/plugin/src/plugin/actions/neo-infobar.ts index c244613c..d5a94ab8 100644 --- a/packages/plugin/src/plugin/actions/neo-infobar.ts +++ b/packages/plugin/src/plugin/actions/neo-infobar.ts @@ -2,13 +2,13 @@ import type { JsonObject } from "@elgato/utils"; import type { Coordinates, FeedbackPayload, WillAppear } from "../../api/index.js"; import { connection } from "../connection.js"; -import { Action } from "./action.js"; +import { ActionBase } from "./action-base.js"; /** * Provides a contextualized instance of an infobar action found on Stream Deck Neo. * @template T The type of settings associated with the action. */ -export class NeoInfobarAction extends Action { +export class NeoInfobarAction extends ActionBase { /** * Private backing field for the coordinates. */ diff --git a/packages/plugin/src/plugin/actions/service.ts b/packages/plugin/src/plugin/actions/service.ts index 5b5aee34..6dd0e2e7 100644 --- a/packages/plugin/src/plugin/actions/service.ts +++ b/packages/plugin/src/plugin/actions/service.ts @@ -30,7 +30,8 @@ import type { import { getManifest } from "../manifest.js"; import { settings } from "../settings.js"; import { ui } from "../ui.js"; -import { Action } from "./action.js"; +import { ActionBase } from "./action-base.js"; +import type { Action } from "./action.js"; import { settingsCache } from "./cache.js"; import { actionConfig } from "./config.js"; import { ActionContext } from "./context.js"; @@ -84,7 +85,7 @@ class ActionService extends ReadOnlyActionStore { */ public onDialDown(listener: (ev: DialDownEvent) => void): IDisposable { return connection.disposableOn("dialDown", (ev: DialDown) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action?.isDial()) { listener(new ActionEvent(action, ev)); } @@ -99,7 +100,7 @@ class ActionService extends ReadOnlyActionStore { */ public onDialRotate(listener: (ev: DialRotateEvent) => void): IDisposable { return connection.disposableOn("dialRotate", (ev: DialRotate) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action?.isDial()) { listener(new ActionEvent(action, ev)); } @@ -114,7 +115,7 @@ class ActionService extends ReadOnlyActionStore { */ public onDialUp(listener: (ev: DialUpEvent) => void): IDisposable { return connection.disposableOn("dialUp", (ev: DialUp) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action?.isDial()) { listener(new ActionEvent(action, ev)); } @@ -135,7 +136,7 @@ class ActionService extends ReadOnlyActionStore { return; } - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action) { listener(new ActionEvent(action, ev)); } @@ -150,7 +151,7 @@ class ActionService extends ReadOnlyActionStore { */ public onKeyDown(listener: (ev: KeyDownEvent) => void): IDisposable { return connection.disposableOn("keyDown", (ev: KeyDown) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action?.isKey()) { listener(new ActionEvent(action, ev)); } @@ -165,7 +166,7 @@ class ActionService extends ReadOnlyActionStore { */ public onKeyUp(listener: (ev: KeyUpEvent) => void): IDisposable { return connection.disposableOn("keyUp", (ev: KeyUp) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action?.isKey()) { listener(new ActionEvent(action, ev)); } @@ -173,7 +174,7 @@ class ActionService extends ReadOnlyActionStore { } /** - * Occurs when the user updates an action's title settings in the Stream Deck application. See also {@link Action.setTitle}. + * Occurs when the user updates an action's title settings in the Stream Deck application. See also {@link ActionBase.setTitle}. * @template T The type of settings associated with the action. * @param listener Function to be invoked when the event occurs. * @returns A disposable that, when disposed, removes the listener. @@ -182,7 +183,7 @@ class ActionService extends ReadOnlyActionStore { listener: (ev: TitleParametersDidChangeEvent) => void, ): IDisposable { return connection.disposableOn("titleParametersDidChange", (ev: TitleParametersDidChange) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action) { listener(new ActionEvent(action, ev)); } @@ -197,7 +198,7 @@ class ActionService extends ReadOnlyActionStore { */ public onTouchTap(listener: (ev: TouchTapEvent) => void): IDisposable { return connection.disposableOn("touchTap", (ev: TouchTap) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action?.isDial()) { listener(new ActionEvent(action, ev)); } @@ -213,7 +214,7 @@ class ActionService extends ReadOnlyActionStore { */ public onWillAppear(listener: (ev: WillAppearEvent) => void): IDisposable { return connection.disposableOn("willAppear", (ev: WillAppear) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action) { listener(new ActionEvent(action, ev)); } @@ -299,9 +300,7 @@ class ActionService extends ReadOnlyActionStore { * @param ev Event that contains the controller. * @returns The action instance. */ - #createAction( - ev: WillAppear, - ): DialAction | KeyAction | NeoInfobarAction { + #createAction(ev: WillAppear): Action { switch (ev.payload.controller) { case "Encoder": return new DialAction(ev); @@ -321,11 +320,11 @@ export const actionService = new ActionService(); export { type ActionService }; /** - * Event associated with an {@link Action}. + * Event associated with an {@link ActionBase}. */ type RoutingEvent = { /** - * The {@link Action} the event is associated with. + * The action the event is associated with. */ action: Action | ActionContext; }; diff --git a/packages/plugin/src/plugin/actions/singleton-action.ts b/packages/plugin/src/plugin/actions/singleton-action.ts index 340a304f..9dece123 100644 --- a/packages/plugin/src/plugin/actions/singleton-action.ts +++ b/packages/plugin/src/plugin/actions/singleton-action.ts @@ -1,7 +1,5 @@ import type { Enumerable, JsonObject, JsonValue } from "@elgato/utils"; -import type { DialAction } from "../actions/dial.js"; -import type { KeyAction } from "../actions/key.js"; import type { DialDownEvent, DialRotateEvent, @@ -19,8 +17,8 @@ import type { WillDisappearEvent, } from "../events/index.js"; import type streamDeck from "../index.js"; -import type { Action } from "./action.js"; -import type { NeoInfobarAction } from "./neo-infobar.js"; +import type { Action } from "../index.js"; +import type { ActionBase } from "./action-base.js"; import { actionStore } from "./store.js"; /** @@ -37,8 +35,8 @@ export class SingletonAction { * Gets the visible actions with the `manifestId` that match this instance's. * @returns The visible actions. */ - public get actions(): Enumerable | KeyAction | NeoInfobarAction> { - return actionStore.filter((a) => a.manifestId === this.manifestId); + public get actions(): Enumerable> { + return actionStore.filter((a) => a.manifestId === this.manifestId) as Enumerable>; } /** @@ -70,7 +68,7 @@ export class SingletonAction { public onDidReceiveResources?(ev: DidReceiveResourcesEvent): Promise | void; /** - * Occurs when the settings associated with an action instance are requested using {@link Action.getSettings}, or when the the settings were updated by the property inspector. + * Occurs when the settings associated with an action instance are requested using {@link ActionBase.getSettings}, or when the the settings were updated by the property inspector. * @param ev Information about the event, including the source action and contextual payload information. */ public onDidReceiveSettings?(ev: DidReceiveSettingsEvent): Promise | void; @@ -104,13 +102,13 @@ export class SingletonAction { public onPropertyInspectorDidDisappear?(ev: PropertyInspectorDidDisappearEvent): Promise | void; /** - * Occurs when a message was sent to the plugin _from_ the property inspector. The plugin can also send messages _to_ the property inspector using {@link Action.sendToPropertyInspector}. + * Occurs when a message was sent to the plugin _from_ the property inspector. The plugin can also send messages _to_ the property inspector using {@link ActionBase.sendToPropertyInspector}. * @param ev Information about the event, including the source action and contextual payload information. */ public onSendToPlugin?(ev: SendToPluginEvent): Promise | void; /** - * Occurs when the user updates an action's title settings in the Stream Deck application. See also {@link Action.setTitle}. + * Occurs when the user updates an action's title settings in the Stream Deck application. See also {@link ActionBase.setTitle}. * @param ev Information about the event, including the source action and contextual payload information. */ public onTitleParametersDidChange?(ev: TitleParametersDidChangeEvent): Promise | void; diff --git a/packages/plugin/src/plugin/actions/store.ts b/packages/plugin/src/plugin/actions/store.ts index 6c1c93ce..a097ddd1 100644 --- a/packages/plugin/src/plugin/actions/store.ts +++ b/packages/plugin/src/plugin/actions/store.ts @@ -1,17 +1,13 @@ import { Enumerable, type JsonObject } from "@elgato/utils"; -import type { DialAction } from "./dial.js"; -import type { KeyAction } from "./key.js"; -import type { NeoInfobarAction } from "./neo-infobar.js"; +import type { Action } from "./action.js"; -const __items = new Map | KeyAction | NeoInfobarAction>(); +const __items = new Map>(); /** * Provides a read-only store of Stream Deck actions. */ -export class ReadOnlyActionStore extends Enumerable< - DialAction | KeyAction | NeoInfobarAction -> { +export class ReadOnlyActionStore extends Enumerable> { /** * Initializes a new instance of the {@link ReadOnlyActionStore}. */ @@ -24,10 +20,8 @@ export class ReadOnlyActionStore extends Enumerable< * @param id Identifier of action to search for. * @returns The action, when present; otherwise `undefined`. */ - public getActionById( - id: string, - ): DialAction | KeyAction | NeoInfobarAction | undefined { - return __items.get(id); + public getActionById(id: string): Action | undefined { + return __items.get(id) as Action; } } @@ -47,7 +41,7 @@ class ActionStore extends ReadOnlyActionStore { * Adds the action to the store. * @param action The action. */ - public set(action: DialAction | KeyAction | NeoInfobarAction): void { + public set(action: Action): void { __items.set(action.id, action); } } diff --git a/packages/plugin/src/plugin/settings.ts b/packages/plugin/src/plugin/settings.ts index 65bc144a..54db83d7 100644 --- a/packages/plugin/src/plugin/settings.ts +++ b/packages/plugin/src/plugin/settings.ts @@ -86,7 +86,7 @@ export const settings = { return; } - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action) { listener(new ActionEvent(action, ev)); } diff --git a/packages/plugin/src/plugin/ui.ts b/packages/plugin/src/plugin/ui.ts index 356efcf6..8ad4596d 100644 --- a/packages/plugin/src/plugin/ui.ts +++ b/packages/plugin/src/plugin/ui.ts @@ -70,7 +70,7 @@ class UIController { listener: (ev: PropertyInspectorDidAppearEvent) => void, ): IDisposable { return connection.disposableOn("propertyInspectorDidAppear", (ev) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action) { listener(new ActionWithoutPayloadEvent(action, ev)); } @@ -88,7 +88,7 @@ class UIController { listener: (ev: PropertyInspectorDidDisappearEvent) => void, ): IDisposable { return connection.disposableOn("propertyInspectorDidDisappear", (ev) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action) { listener(new ActionWithoutPayloadEvent(action, ev)); } @@ -106,7 +106,7 @@ class UIController { listener: (ev: SendToPluginEvent) => void, ): IDisposable { return connection.disposableOn("sendToPlugin", (ev) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context); if (action) { listener( new SendToPluginEvent(action, ev as DidReceivePropertyInspectorMessage), From 2d08d584e6efebceaec8184f1c946ccc577bcf8d Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Thu, 20 Aug 2026 17:09:47 +0100 Subject: [PATCH 07/11] fix: types and tests --- .../plugin/actions/__tests__/service.test.ts | 43 ++++++++++--------- packages/plugin/src/plugin/devices/device.ts | 8 +--- packages/plugin/src/plugin/ui.ts | 9 ++-- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/plugin/src/plugin/actions/__tests__/service.test.ts b/packages/plugin/src/plugin/actions/__tests__/service.test.ts index 25c0fd1e..2a0badfa 100644 --- a/packages/plugin/src/plugin/actions/__tests__/service.test.ts +++ b/packages/plugin/src/plugin/actions/__tests__/service.test.ts @@ -37,6 +37,7 @@ import { type WillDisappearEvent, } from "../../events/index.js"; import type { UIController } from "../../ui.js"; +import type { Action } from "../action.js"; import { settingsCache } from "../cache.js"; import { actionConfig } from "../config.js"; import { ActionContext } from "../context.js"; @@ -108,7 +109,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialDownEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialDown", }); @@ -154,7 +155,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialRotateEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialRotate", }); @@ -198,7 +199,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialUpEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialUp", }); @@ -243,7 +244,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[KeyDownEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "keyDown", }); @@ -288,7 +289,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[KeyUpEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "keyUp", }); @@ -342,7 +343,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[TitleParametersDidChangeEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "titleParametersDidChange", }); @@ -388,7 +389,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[TouchTapEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "touchTap", }); @@ -433,7 +434,7 @@ describe("actions", () => { // Assert (emit). expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[WillAppearEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "willAppear", }); @@ -658,7 +659,7 @@ describe("actions", () => { describe("registering an action", () => { const keyManifestId = "com.elgato.test.key"; const dialManifestId = "com.elgato.test.dial"; - const actions = vi.fn() as unknown as Enumerable | KeyAction>; + const actions = vi.fn() as unknown as Enumerable>; /** * Asserts {@link ActionService.registerAction} validates the manifest identifier is not undefined. @@ -752,7 +753,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialDownEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialDown", }); @@ -796,7 +797,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialRotateEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialRotate", }); @@ -838,7 +839,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DialUpEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "dialUp", }); @@ -871,7 +872,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[SendToPluginEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: { name: "Hello world", }, @@ -916,7 +917,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[DidReceiveSettingsEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "didReceiveSettings", }); @@ -959,7 +960,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[KeyDownEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "keyDown", }); @@ -1002,7 +1003,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[KeyUpEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "keyUp", }); @@ -1033,7 +1034,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[PropertyInspectorDidAppearEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, type: "propertyInspectorDidAppear", }); }); @@ -1063,7 +1064,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[PropertyInspectorDidDisappearEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, type: "propertyInspectorDidDisappear", }); }); @@ -1114,7 +1115,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[TitleParametersDidChangeEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "titleParametersDidChange", }); @@ -1158,7 +1159,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[TouchTapEvent]>({ - action: actionStore.getActionById(ev.context) as DialAction, + action: actionStore.getActionById(ev.context) as DialAction, payload: ev.payload, type: "touchTap", }); @@ -1201,7 +1202,7 @@ describe("actions", () => { // Assert. expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith<[WillAppearEvent]>({ - action: actionStore.getActionById(ev.context) as KeyAction, + action: actionStore.getActionById(ev.context) as KeyAction, payload: ev.payload, type: "willAppear", }); diff --git a/packages/plugin/src/plugin/devices/device.ts b/packages/plugin/src/plugin/devices/device.ts index 46e27234..aed750f2 100644 --- a/packages/plugin/src/plugin/devices/device.ts +++ b/packages/plugin/src/plugin/devices/device.ts @@ -1,9 +1,7 @@ import type { JsonObject } from "@elgato/utils"; import type { DeviceInfo, DeviceType, Size } from "../../api/index.js"; -import type { DialAction } from "../actions/dial.js"; -import type { KeyAction } from "../actions/key.js"; -import type { NeoInfobarAction } from "../actions/neo-infobar.js"; +import type { Action } from "../actions/action.js"; import { actionStore } from "../actions/store.js"; import { connection } from "../connection.js"; @@ -64,9 +62,7 @@ export class Device { * Actions currently visible on the device. * @returns Collection of visible actions. */ - public get actions(): IterableIterator< - DialAction | KeyAction | NeoInfobarAction - > { + public get actions(): IterableIterator> { return actionStore.filter((a) => a.device.id === this.id); } diff --git a/packages/plugin/src/plugin/ui.ts b/packages/plugin/src/plugin/ui.ts index 8ad4596d..f4c008ef 100644 --- a/packages/plugin/src/plugin/ui.ts +++ b/packages/plugin/src/plugin/ui.ts @@ -1,8 +1,7 @@ import type { IDisposable, JsonObject, JsonValue } from "@elgato/utils"; import type { DidReceivePropertyInspectorMessage } from "../api/index.js"; -import type { DialAction, KeyAction } from "./actions/index.js"; -import type { NeoInfobarAction } from "./actions/neo-infobar.js"; +import type { Action } from "./actions/index.js"; import { actionStore } from "./actions/store.js"; import { connection } from "./connection.js"; import { ActionWithoutPayloadEvent } from "./events/action-event.js"; @@ -19,7 +18,7 @@ class UIController { /** * Action associated with the current property inspector. */ - #action: DialAction | KeyAction | NeoInfobarAction | undefined; + #action: Action | undefined; /** * To overcome event races, the debounce counter keeps track of appear vs disappear events, ensuring @@ -55,7 +54,7 @@ class UIController { * Gets the action associated with the current property. * @returns The action; otherwise `undefined` when a property inspector is not visible. */ - public get action(): DialAction | KeyAction | NeoInfobarAction | undefined { + public get action(): Action | undefined { return this.#action; } @@ -135,7 +134,7 @@ class UIController { * @param action Action to check against. * @returns `true` when the actions are the same. */ - #isCurrent(action: DialAction | KeyAction | NeoInfobarAction): boolean { + #isCurrent(action: Action): boolean { return ( this.#action?.id === action.id && this.#action?.manifestId === action.manifestId && From 6cf811bf7ad0924e1befc2dd4a83b0e2492f16fa Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 21 Aug 2026 18:37:07 +0100 Subject: [PATCH 08/11] test: fix types --- packages/plugin/src/plugin/actions/__tests__/action.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin/src/plugin/actions/__tests__/action.test.ts b/packages/plugin/src/plugin/actions/__tests__/action.test.ts index d26070ca..61ffb530 100644 --- a/packages/plugin/src/plugin/actions/__tests__/action.test.ts +++ b/packages/plugin/src/plugin/actions/__tests__/action.test.ts @@ -291,7 +291,7 @@ describe("Action", () => { }); describe("sending", () => { - let action!: KeyAction; + let action!: KeyAction; beforeAll(() => (action = new KeyAction(source))); /** From a57429a56eba94e76e1d0159c212e40e6a8d380a Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 21 Aug 2026 18:41:42 +0100 Subject: [PATCH 09/11] docs: update changesets --- .changeset/pink-jars-admire.md | 2 +- .changeset/wild-bikes-vanish.md | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .changeset/wild-bikes-vanish.md diff --git a/.changeset/pink-jars-admire.md b/.changeset/pink-jars-admire.md index cb5b87a0..6309c5d4 100644 --- a/.changeset/pink-jars-admire.md +++ b/.changeset/pink-jars-admire.md @@ -2,4 +2,4 @@ "@elgato/streamdeck": major --- -Updated the settings type used by `getSettings` and `setSettings` to use the type defined on the action. +Updated `getSettings` and `setSettings` so that the settings type is correctly bound to the type defined on the action. diff --git a/.changeset/wild-bikes-vanish.md b/.changeset/wild-bikes-vanish.md deleted file mode 100644 index b0dc62ab..00000000 --- a/.changeset/wild-bikes-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@elgato/streamdeck": major ---- - -Fixed type-narrowing when calling `setSettings` on an event's action. From 80c6bf104134bcac775dcc33d14727aff1090f25 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 21 Aug 2026 18:47:27 +0100 Subject: [PATCH 10/11] refactor: remove type obfuscation from getActionById --- packages/plugin/src/plugin/actions/service.ts | 18 +++++++++--------- packages/plugin/src/plugin/actions/store.ts | 4 ++-- packages/plugin/src/plugin/settings.ts | 3 ++- packages/plugin/src/plugin/ui.ts | 6 +++--- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/plugin/src/plugin/actions/service.ts b/packages/plugin/src/plugin/actions/service.ts index 6dd0e2e7..6338e509 100644 --- a/packages/plugin/src/plugin/actions/service.ts +++ b/packages/plugin/src/plugin/actions/service.ts @@ -85,7 +85,7 @@ class ActionService extends ReadOnlyActionStore { */ public onDialDown(listener: (ev: DialDownEvent) => void): IDisposable { return connection.disposableOn("dialDown", (ev: DialDown) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action?.isDial()) { listener(new ActionEvent(action, ev)); } @@ -100,7 +100,7 @@ class ActionService extends ReadOnlyActionStore { */ public onDialRotate(listener: (ev: DialRotateEvent) => void): IDisposable { return connection.disposableOn("dialRotate", (ev: DialRotate) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action?.isDial()) { listener(new ActionEvent(action, ev)); } @@ -115,7 +115,7 @@ class ActionService extends ReadOnlyActionStore { */ public onDialUp(listener: (ev: DialUpEvent) => void): IDisposable { return connection.disposableOn("dialUp", (ev: DialUp) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action?.isDial()) { listener(new ActionEvent(action, ev)); } @@ -136,7 +136,7 @@ class ActionService extends ReadOnlyActionStore { return; } - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action) { listener(new ActionEvent(action, ev)); } @@ -151,7 +151,7 @@ class ActionService extends ReadOnlyActionStore { */ public onKeyDown(listener: (ev: KeyDownEvent) => void): IDisposable { return connection.disposableOn("keyDown", (ev: KeyDown) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action?.isKey()) { listener(new ActionEvent(action, ev)); } @@ -166,7 +166,7 @@ class ActionService extends ReadOnlyActionStore { */ public onKeyUp(listener: (ev: KeyUpEvent) => void): IDisposable { return connection.disposableOn("keyUp", (ev: KeyUp) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action?.isKey()) { listener(new ActionEvent(action, ev)); } @@ -183,7 +183,7 @@ class ActionService extends ReadOnlyActionStore { listener: (ev: TitleParametersDidChangeEvent) => void, ): IDisposable { return connection.disposableOn("titleParametersDidChange", (ev: TitleParametersDidChange) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action) { listener(new ActionEvent(action, ev)); } @@ -198,7 +198,7 @@ class ActionService extends ReadOnlyActionStore { */ public onTouchTap(listener: (ev: TouchTapEvent) => void): IDisposable { return connection.disposableOn("touchTap", (ev: TouchTap) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action?.isDial()) { listener(new ActionEvent(action, ev)); } @@ -214,7 +214,7 @@ class ActionService extends ReadOnlyActionStore { */ public onWillAppear(listener: (ev: WillAppearEvent) => void): IDisposable { return connection.disposableOn("willAppear", (ev: WillAppear) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action) { listener(new ActionEvent(action, ev)); } diff --git a/packages/plugin/src/plugin/actions/store.ts b/packages/plugin/src/plugin/actions/store.ts index a097ddd1..b5093b8e 100644 --- a/packages/plugin/src/plugin/actions/store.ts +++ b/packages/plugin/src/plugin/actions/store.ts @@ -20,8 +20,8 @@ export class ReadOnlyActionStore extends Enumerable> { * @param id Identifier of action to search for. * @returns The action, when present; otherwise `undefined`. */ - public getActionById(id: string): Action | undefined { - return __items.get(id) as Action; + public getActionById(id: string): Action | undefined { + return __items.get(id); } } diff --git a/packages/plugin/src/plugin/settings.ts b/packages/plugin/src/plugin/settings.ts index 54db83d7..48f6fa87 100644 --- a/packages/plugin/src/plugin/settings.ts +++ b/packages/plugin/src/plugin/settings.ts @@ -2,6 +2,7 @@ import type { IDisposable, JsonObject } from "@elgato/utils"; import { randomUUID } from "node:crypto"; import type { DidReceiveGlobalSettings, DidReceiveSettings } from "../api/index.js"; +import type { Action } from "./actions/action.js"; import { actionConfig } from "./actions/config.js"; import { actionStore } from "./actions/store.js"; import { connection } from "./connection.js"; @@ -86,7 +87,7 @@ export const settings = { return; } - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action) { listener(new ActionEvent(action, ev)); } diff --git a/packages/plugin/src/plugin/ui.ts b/packages/plugin/src/plugin/ui.ts index f4c008ef..d64a7ec0 100644 --- a/packages/plugin/src/plugin/ui.ts +++ b/packages/plugin/src/plugin/ui.ts @@ -69,7 +69,7 @@ class UIController { listener: (ev: PropertyInspectorDidAppearEvent) => void, ): IDisposable { return connection.disposableOn("propertyInspectorDidAppear", (ev) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action) { listener(new ActionWithoutPayloadEvent(action, ev)); } @@ -87,7 +87,7 @@ class UIController { listener: (ev: PropertyInspectorDidDisappearEvent) => void, ): IDisposable { return connection.disposableOn("propertyInspectorDidDisappear", (ev) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action) { listener(new ActionWithoutPayloadEvent(action, ev)); } @@ -105,7 +105,7 @@ class UIController { listener: (ev: SendToPluginEvent) => void, ): IDisposable { return connection.disposableOn("sendToPlugin", (ev) => { - const action = actionStore.getActionById(ev.context); + const action = actionStore.getActionById(ev.context) as Action | undefined; if (action) { listener( new SendToPluginEvent(action, ev as DidReceivePropertyInspectorMessage), From 4bd7a5de120cf70835c35fa3e4913fab3f7094ce Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 21 Aug 2026 18:48:01 +0100 Subject: [PATCH 11/11] chore: revert ignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 7e53fa2d..6e88c1f6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,6 @@ dist/ bin/ .wireit/ -# Currently reserved for local testing -apps/ - # Shared package files /packages/*/LICENSE /packages/plugin/README.md