Shipmind Labs

QA filed one line: the phone was opened from the screen, then left on the desk, and it never locked itself again.

We spent most of a day reading the timer code. The timer was fine.

Our idle poll only got rescheduled when the app's foreground/background state changed. That covered the normal case, since people usually leave by switching away. But opening the lock directly from inside the app moved it without producing any event the poll was listening to. So the session sat open and unpolled until something unrelated woke the app. Backgrounding it once and coming back "fixed" the bug, and that is exactly why it looked like a timer problem.

The bug was not in the timer. It was in where we announced the change.

In our open-source project sessionlock we moved the notification out of the individual methods and put it underneath them. The lock keeps a set of listeners, and subscribing returns the function that removes you. Every path that can move the lock captures the current state and reason first, mutates, then calls one private announce, which reads the snapshot once and returns without calling anybody when both the state and the reason are unchanged. Locking an already-locked session stays silent. Opening a session that is already open stays silent. A real transition reports the new snapshot.

One more rule did most of the work: there is exactly one private path by which the lock shuts, and both the public lock and the restore route go through it. So a method somebody adds next quarter is covered without them knowing the notification exists.

The poll then just subscribes. It clears any outstanding handle, returns early if the lock is locked, and otherwise wakes at the larger of 250 milliseconds and the time left until idle lock. Subscribing happens before the app-state listener is registered, so opening the session directly arms a timer immediately and locking it directly cancels it. Teardown unsubscribes first, then removes the app-state subscription, then clears the handle, because a detached lock should never wake a timer.

What changed in how we work: when a state machine has more than one entry point, the notification goes below the methods rather than inside them, and the deduplication compares the real snapshot instead of trusting whoever called. Otherwise every new method is a chance to forget.

We would be glad to hear where you put change notification in your own state machines, in each mutator or in one place underneath them.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com