Skip to content

v4.2.0

Latest

Choose a tag to compare

@camcima camcima released this 19 Aug 11:04
· 1 commit to main since this release

Remediation of the 2026-08 architecture review (#52). Two reproduced bugs fixed, four API-consistency gaps closed, no breaking changes. Coverage of src/ remains 100% across 389 tests.

Fixed

A failed lock release is no longer silent. MutexInterface.releaseLock() signals failure two ways — it throws, or it returns false. Only the throw was handled. A false return (what a PostgreSQL advisory unlock or a Redis DEL that removed nothing produces) meant the operation resolved successfully, onReleaseError never fired, and isLockAcquired() stayed true forever — after which every subsequent operation piggybacked on the stuck lock and never released it. Both failure modes now raise the new LockCanNotBeReleasedError.

Behavior change to check if you use a custom mutex. An operation whose release fails now rejects where it previously resolved. Implementations conforming to MaybePromise<boolean> are unaffected, and the default NullMutex always returns true — but a mutex returning undefined (untyped JavaScript, or a loose test double) will now surface an error instead of passing silently.

whenIdle() no longer deadlocks. It was missing the re-entrancy guard triggerEvent/checkTransitions received in v4. Awaiting it inside an observer could never resolve — the machine cannot reach idle while the runner is blocked on that very callback — so the machine wedged permanently and silently. It now throws ReentrancyError.

LockAdapterMutex no longer double-acquires. The acquired flag is only set after the adapter resolves, so two overlapping acquireLock() calls both passed the guard and acquired twice on a non-idempotent adapter. Overlapping calls now share one in-flight acquire; a failed acquire is still retryable.

Observer accessors return snapshots. getBeforeObservers(), getAfterObservers(), and Event.getObservers() handed out their live collections, so a later detach mutated a list a caller already held.

Added

  • Factory accepts engine options — a third constructor argument typed FactoryStatemachineOptions, forwarding maxQueueLength, maxAutomaticHops, autoreleaseLock, onChainedOperationError, and onReleaseError to every machine it creates. Previously factory-created machines silently ran on defaults, with no back-pressure and no diagnostic sinks — precisely the fleet-of-machines case those options exist for. The three fields the factory derives per subject (initialStateName, mutex, transitionSelector) are excluded at the type level.
  • LockCanNotBeReleasedError — typed error for the failed-release path, with code lockCanNotBeReleased.
  • AmbiguousTransitionError.candidates — the competing transitions (target, event, condition, weight), also rendered into the message. A bare count never identified the culprits.
  • Statemachine.releaseLock() now reports failures through onReleaseError. Its Promise<void> signature is unchanged; returning the boolean is breaking and is queued for v5 (#55).

Documentation

Release-failure semantics, the factory options template, whenIdle re-entrancy, and two things easy to misread from the API alone: Timeout schedules nothing — it only fires when something drives the machine — and event observers are shared by every machine built from the same process.

Also

Deprecation notices previously marked "removed in v4" are re-dated to v5, and the Codecov project floor moved from 80% to 95% (#65).

Outstanding work from the review is tracked in #64, with the breaking items batched under the v5 milestone.

Full changelog: v4.1.0...v4.2.0