Alex
Alex

Reputation: 2072

worker-timers package callback fires immediately after deep sleep, ignoring intended delay

I’m using the worker-timers npm package in a Vue 3 + TypeScript application to implement a lock/unlock mechanism of notes using Ably (where the notes are shared between multiple users). The idea is that when a user focuses on a note editor, it is locked by that user (implementation is not relevant to the question/issue). After the note is locked and I start an inactivity timer (5 minutes by default). If the user doesn’t interact with the editor for that duration, the note should unlock automatically.

Usage flow:

User focuses on the note’s editor (e.g., clicking in an editable area). I create a fresh instance of an InactivityTimer class each time the editor is focused. This timer uses workerSetTimeout from worker-timers to schedule a callback after timeoutDuration milliseconds. When the callback fires, it triggers onInactive which unlocks the note. If the user interacts again (e.g., typing or focusing), I reset or re-init the timer. This works fine under normal circumstances, including switching tabs or move to different app. worker-timers helps avoid some throttling issues. However, after the system goes into a deep sleep (e.g., the machine was sleeping for some time), once the user returns and tries to lock a note by focusing on it, the inactivity callback fires immediately instead of waiting the configured 5 minutes.

What I’ve tried:

None of these attempts have solved the issue. After system resume, the scheduled timeout seems to consider the intended delay already passed, causing the callback to fire instantly.

import { setTimeout as workerSetTimeout, clearTimeout as workerClearTimeout } from "worker-timers";

interface InactivityOptions {
    timeout: number
    onInactive: () => Promise<void> | void
}

type InactivityCallback = (() => Promise<void> | void) | null

export class InactivityTimer {
    private DEFAULT_TIMEOUT = 5 * 60 * 1000;
    private THIRTY_SEC_TIMEOUT = 30 * 1000;
    private inactivityTimerId: number | null = null;
    private warningTimerId: number | null = null;
    private inactivityCallback: InactivityCallback = null;
    private timeoutDuration = this.DEFAULT_TIMEOUT;
    public showWarning = false;

    private clearTimers() {
        if (this.warningTimerId !== null) {
            workerClearTimeout(this.warningTimerId);
            this.warningTimerId = null;
        }
        if (this.inactivityTimerId !== null) {
            workerClearTimeout(this.inactivityTimerId);
            this.inactivityTimerId = null;
        }
    }

    public resetTimer() {
        this.clearTimers();
        this.showWarning = false;

        if (this.inactivityCallback) {
            this.inactivityTimerId = workerSetTimeout(() => {
                this.showWarning = true;
                this.warningTimerId = workerSetTimeout(() => {
                    this.showWarning = false;
                    if (this.inactivityCallback) {
                        this.inactivityCallback();
                    }
                }, this.THIRTY_SEC_TIMEOUT);
            }, 5 * 60 * 1000);
        }
    }

    public initInactivityDetector({ timeout, onInactive }: InactivityOptions) {
        this.timeoutDuration = timeout > 0 ? timeout : this.DEFAULT_TIMEOUT;
        this.inactivityCallback = onInactive;
        this.resetTimer();
    }

    public stopInactivityDetector() {
        this.clearTimers();
        this.inactivityCallback = null;
        this.showWarning = false;
    }
}

this method is run when user clicks on a note to lock it:

const inactivityTimerRef = ref<InactivityTimer | null>(null);

const onInactive = async (noteId: string) => {
  // code that unlocks the note
};

const onEditorFocus = async () => {
        if (locked.value) return;

        const newTimer = new InactivityTimer();
        inactivityTimerRef.value = newTimer;
        isEditorOnFocus.value = true;

        await setFocus(note.value.id);

        newTimer.initInactivityDetector({
            timeout: UNLOCK_NOTE_TIMEOUT,
            onInactive: () => onInactive(note.value.id),
        });

        newTimer.resetTimer();
    };

The problem:

After waking from deep sleep or significant browser suspension, focusing the note again triggers the inactivity callback immediately instead of waiting 5 minutes. It seems the worker timer treats the scheduled time as already elapsed.

Upvotes: 0

Views: 26

Answers (0)

Related Questions