Skip to content

Provide a scheduler helper for Timeout-driven transitions #61

Description

@camcima

From the improvement opportunities of the 2026-08 architecture review. #52 documented the behavior; this is the ergonomic follow-up.

Problem

Timeout reads as a scheduling primitive and is not one. It is a plain condition: it compares getLastStateHasChangedDate() + timeoutMs against Date.now() when something asks. Nothing fires at the deadline. A transition guarded by Timeout fires only when someone calls checkTransitions() after the deadline has passed.

The name invites the wrong mental model, and the failure is silent — the workflow simply never advances, which is easy to miss in development where a test calls checkTransitions() explicitly and hard to diagnose in production where nothing does. #52 added an explicit warning and a polling snippet to docs/conditions.md, but every user still hand-rolls the same loop.

Proposed direction

A small opt-in driver, kept out of the core so the zero-dependency, runtime-agnostic guarantee holds:

const stop = pollTransitions(sm, { intervalMs: 60_000, onError: log });
// later
stop();

Design points worth settling:

  • Skip a tick while the machine is still draining (coordinate with whenIdle() / the isRunning() accessor proposed in Expose queue depth and runner state for observability #60) so slow operations cannot pile up overlapping checkTransitions() calls.
  • Route rejections to a caller-supplied handler; an unhandled rejection inside a timer is a bad default.
  • unref() the timer under Node so it cannot hold a process open — behind a capability check, not a bare Node import.
  • Consider deriving the next tick from the nearest Timeout deadline instead of a fixed interval, if that can be done without coupling the driver to condition internals.

An alternative worth weighing first: rename or alias the condition (ElapsedSince?) so the name stops implying a scheduler at all. That is breaking, so it belongs with the v5 batch if chosen — but it may be the more honest fix, with the helper as a complement rather than a substitute.

Acceptance criteria

  • A documented way to drive timeout transitions without hand-rolling a polling loop.
  • Core remains dependency-free and free of unconditional Node built-ins.
  • Overlapping ticks cannot queue up behind a slow operation.
  • docs/conditions.md points at the helper from the existing warning.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions