v1.0.40: fix duplicate timer race condition on rapid double-press

onKeyDown is async and calls await startTimer (~1s network). A second
press before that resolves saw the same state (isRunning=false,
memRunningEntryId=null) and created a second Notion entry. Only the
last startTimer call's ID was tracked, orphaning the first entry
running indefinitely in Notion.

pendingKeyDown Set acts as a per-action mutex: a second press while the
first is in-flight is dropped. try/finally guarantees the lock is always
released.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
pdmarf
2026-05-13 18:18:52 +01:00
parent 060a2bc917
commit 6e0976a284
3 changed files with 60 additions and 46 deletions

View File

@@ -6438,7 +6438,7 @@ async function stopTimer(token, entryId) {
} }
// src/plugin.ts // src/plugin.ts
var CURRENT_VERSION = "1.0.39"; var CURRENT_VERSION = "1.0.40";
var GITEA_BASE = "https://gitea.pdmarf.co.uk/pdm/stream_deck_notion_timer/raw/branch/stable-rebuild"; var GITEA_BASE = "https://gitea.pdmarf.co.uk/pdm/stream_deck_notion_timer/raw/branch/stable-rebuild";
var SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- var SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAN7ko8TUpuPzPAJuKAZCRjV0c4ZSlou5d9pUAF6o12b4= MCowBQYDK2VwAyEAN7ko8TUpuPzPAJuKAZCRjV0c4ZSlou5d9pUAF6o12b4=
@@ -6569,6 +6569,7 @@ function buttonTitle(projectName) {
} }
var TimerToggle = class extends SingletonAction { var TimerToggle = class extends SingletonAction {
projectCache = /* @__PURE__ */ new Map(); projectCache = /* @__PURE__ */ new Map();
pendingKeyDown = /* @__PURE__ */ new Set();
async onWillAppear(ev) { async onWillAppear(ev) {
this.projectCache.set(ev.action.id, ev.payload.settings); this.projectCache.set(ev.action.id, ev.payload.settings);
const title = buttonTitle(ev.payload.settings.projectName || ""); const title = buttonTitle(ev.payload.settings.projectName || "");
@@ -6584,6 +6585,9 @@ var TimerToggle = class extends SingletonAction {
await sendProjectsToPI(); await sendProjectsToPI();
} }
async onKeyDown(ev) { async onKeyDown(ev) {
if (this.pendingKeyDown.has(ev.action.id)) return;
this.pendingKeyDown.add(ev.action.id);
try {
const { projectId, projectName } = ev.payload.settings; const { projectId, projectName } = ev.payload.settings;
const title = buttonTitle(projectName || ""); const title = buttonTitle(projectName || "");
const isRunning = memRunningActionId === ev.action.id; const isRunning = memRunningActionId === ev.action.id;
@@ -6631,6 +6635,9 @@ var TimerToggle = class extends SingletonAction {
plugin_default.logger.error("Timer toggle failed:", err); plugin_default.logger.error("Timer toggle failed:", err);
await ev.action.showAlert(); await ev.action.showAlert();
} }
} finally {
this.pendingKeyDown.delete(ev.action.id);
}
} }
}; };
TimerToggle = __decorateClass([ TimerToggle = __decorateClass([

View File

@@ -1 +1 @@
в<╥!Ъ░xR√з└С;=▀LI" я╓╢HuэF├ы▐Ос─╒┘ sb▌Y╛{йH│╤Н.JщNЦь1╔l {¿óîj¶? ÍzDA+ÌOÎi °<u,&/Õ+ødeÆá¤"÷œSw+&öO.RC& óä^H-¯

View File

@@ -1,4 +1,4 @@
const CURRENT_VERSION = "1.0.39"; const CURRENT_VERSION = "1.0.40";
const GITEA_BASE = "https://gitea.pdmarf.co.uk/pdm/stream_deck_notion_timer/raw/branch/stable-rebuild"; const GITEA_BASE = "https://gitea.pdmarf.co.uk/pdm/stream_deck_notion_timer/raw/branch/stable-rebuild";
const SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- const SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAN7ko8TUpuPzPAJuKAZCRjV0c4ZSlou5d9pUAF6o12b4= MCowBQYDK2VwAyEAN7ko8TUpuPzPAJuKAZCRjV0c4ZSlou5d9pUAF6o12b4=
@@ -167,6 +167,7 @@ function buttonTitle(projectName: string): string {
@action({ UUID: "com.pdma.notion-timer.toggle" }) @action({ UUID: "com.pdma.notion-timer.toggle" })
class TimerToggle extends SingletonAction<TimerSettings> { class TimerToggle extends SingletonAction<TimerSettings> {
private projectCache = new Map<string, TimerSettings>(); private projectCache = new Map<string, TimerSettings>();
private pendingKeyDown = new Set<string>();
async onWillAppear(ev: WillAppearEvent<TimerSettings>): Promise<void> { async onWillAppear(ev: WillAppearEvent<TimerSettings>): Promise<void> {
this.projectCache.set(ev.action.id, ev.payload.settings); this.projectCache.set(ev.action.id, ev.payload.settings);
@@ -185,6 +186,9 @@ class TimerToggle extends SingletonAction<TimerSettings> {
} }
async onKeyDown(ev: KeyDownEvent<TimerSettings>): Promise<void> { async onKeyDown(ev: KeyDownEvent<TimerSettings>): Promise<void> {
if (this.pendingKeyDown.has(ev.action.id)) return;
this.pendingKeyDown.add(ev.action.id);
try {
const { projectId, projectName } = ev.payload.settings; const { projectId, projectName } = ev.payload.settings;
const title = buttonTitle(projectName || ""); const title = buttonTitle(projectName || "");
const isRunning = memRunningActionId === ev.action.id; const isRunning = memRunningActionId === ev.action.id;
@@ -231,6 +235,9 @@ class TimerToggle extends SingletonAction<TimerSettings> {
streamDeck.logger.error("Timer toggle failed:", err); streamDeck.logger.error("Timer toggle failed:", err);
await ev.action.showAlert(); await ev.action.showAlert();
} }
} finally {
this.pendingKeyDown.delete(ev.action.id);
}
} }
} }