All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
2.5.0 - 2026-08-25
Added
Elixir 1.19 and 1.20 to the CI matrix, which now spans the declared floor (
~> 1.15) through the current release. The two most recent Elixir versions — the ones most adopters run — were previously untested, and the set-theoretic type checker is exactly the kind of moving target a macro-heavy library needs coverage against. Thelintflag moved to the newest entry so formatting and Credo run against current tooling, and the Dialyzer job now matches.tool-versionsinstead of trailing the matrix by two releases. (Issue #23)A Troubleshooting guide, opening with "Why does the compiler say my pattern will never match?" (Issue #24)
usage-rules.md, shipped in the package — WaitForIt's guidance condensed for AI coding agents, in the layoutusage_rulessyncs from. A consumer can pull it into theirAGENTS.mdwithmix usage_rules.sync, or read it directly atdeps/wait_for_it/usage-rules.md.It leads with the one rule — on timeout each form behaves as its native counterpart would on a final non-matching evaluation — and then the traps that are easy to get wrong and silent when you do: a catch-all clause in
case_wait/cond_waitdisables the waiting entirely (measured: it halts after one evaluation in 4 ms), waiting blocks the calling process and must not happen inside a GenServer callback, signals are node-local, andtimeout: :infinityremoves all timeout behaviour rather than merely extending it.
Changed
Telemetry metadata
envis now a trimmed map rather than the caller's wholeMacro.Env(Issue #22). It carries exactly:context,:context_modules,:file,:function,:line, and:module— the same six fieldsWaitForIt.TimeoutErrorhas always exposed. The two paths disagreed about whatenvmeant; now they share one definition and cannot drift apart again.A full
__ENV__measured 1019 words (~8 KB) in a module with ordinary imports, of which most is the calling module's import table (:functions,:macros,:requires) — it grows with the caller's imports and is of no use to a handler. That term was embedded in the caller's compiled module once per wait, copied into the signal registry's ETS on every signal-based wait, and copied again by every handler that forwarded metadata off-process.The trim happens at macro expansion, so the fat term is never emitted rather than being discarded later. Measured on a module with three wait sites:
before after envterm1019 words 29 words whole event metadata 1039 words 49 words caller's compiled beam 12,612 bytes 3,360 bytes A handler reading anything outside those six keys needs updating; one reading
env.fileorenv.linedoes not. The value passed down internally stays a real%Macro.Env{}so thatMacro.Env.stacktrace/1still reraises a timed-outmatch_wait/case_wait/cond_waitat the caller's location — byte-for-byte the stacktrace it produced before.
Fixed
The test suite no longer emits "the following clause will never match" warnings on Elixir 1.20. These came from the suite's own stub helpers, not from the waiting macros: 1.20 infers an exact return type for a local function, so
defp pending, do: :pendinghas the type:pending, and a wait for{:ok, _}on it genuinely cannot match. The helpers wanted a runtime non-match to drive the timeout paths, not a type-level one, so they now route through the process dictionary — same value, no inferable type.Characterised across waitable shapes before changing anything: the warning fires only when the expression's inferred type is disjoint from the pattern. A union that includes the pattern, an opaque value, a
@spec'dterm(), thenil | structshape ofRepo.get/2, and a tagged{:ok, _} | {:error, _}union are all clean, as arewait/2andcase_wait/3. So no realistic waitable trips it, and when it does fire it is correct — waiting changes values over time, not types, and an expression whose type cannot produce the pattern can only ever time out.WaitForIt therefore does not suppress the diagnostic in its expansion. The mechanism that produces the surprising warning is the same one that catches a genuinely impossible pattern in user code; silencing it library-wide would trade a rare accurate warning for a permanent blind spot. The guide explains the diagnosis and the fix instead. (Issue #24)
2.4.0 - 2026-07-31
Added
- The
:timeoutoption now accepts:infinity, for a wait that continues until its condition is met rather than giving up after a fixed budget. Such a wait can never time out, so the!variants never raise,elseclauses never run, andWaitForIt.until/2never returns{:timeout, last_value}. (Issue #6) - Telemetry metadata now includes a
wait_contextkey, which distinguishes a wait that was written directly (nil) from one a construct desugared to. A<~clause of awith_waitreports%{construct: :with_wait, clause: index}, so its events are no longer indistinguishable from a standalonematch_waitand can be attributed to a specific clause. (Issue #19)
Fixed
- Corrected the
WaitForIt.wait_opt/0typespec, which declared:interval(and its:frequencyalias) as an integer only, even though aWaitForIt.Backofffunction has been accepted since 2.2.0.
2.3.0 - 2026-07-31
Added
- Added a functional (non-macro) waiting API,
WaitForIt.until/2andWaitForIt.until!/2, for conditions that are computed at runtime or passed in as a function.until/2returns a tagged{:ok, value}or{:timeout, last_value}result;until!/2returns the bare value on success and raisesWaitForIt.TimeoutErroron timeout.
Changed
- Clarified the "Timeout behavior" documentation to lead with the single underlying rule — on timeout, each waiting form behaves exactly as its native Elixir counterpart would on a final non-matching evaluation — and demoted the behavior matrix to a reference table (now with a native-counterpart column). Documentation only; no API or behavioral changes.
2.2.1 - 2026-06-18
Changed
- Restructured the documentation so the README is the single source for the
WaitForItmodule documentation, removing the duplicated Overview page. Documentation only; no API or behavioral changes.
2.2.0 - 2026-06-18
Added
- Added the
with_wait/3andwith_wait!/3macros for composing several waits in awith-style pipeline. Clauses use<-(ordinary one-shot match) or<~(wait-for-match, with optional per-clause options); a<~timeout flows to theelseblock like an ordinary non-match (or raisesWaitForIt.TimeoutErrorforwith_wait!). See the "Composing waits" guide. - Added the
WaitForIt.Testmodule withassert_eventually/2(truthy and pattern-binding forms),refute_eventually/2, andassert_always/2test assertions that fail with a regularExUnit.AssertionError(including the source expression and last value) on timeout. - Documented and promoted the
match_wait/3construct, and added amatch_wait!/3bang variant. - Added the
:intervaloption as the preferred name for the polling interval.:frequencycontinues to work as an alias and is slated for removal in a future major version. Added
:telemetryevents ([:wait_for_it, :wait, :start | :stop | :exception]) for every wait, exposing wait duration, evaluation count, and outcome.- Added backoff support: the
:intervaloption now accepts a 1-arity function of the attempt number, plus a newWaitForIt.Backoffmodule withconstant/1andexponential/1strategies. - Added guides (Waiting in tests, Polling vs signaling, Composing waits, Recipes, Telemetry) and a rewritten README.
- Added GitHub Actions CI (test matrix, formatting, Credo, Dialyzer).
Changed
- Rewrote the internal wait loop to use a single monotonic deadline, making timeouts immune to wall-clock adjustments and unifying the polling and signaling code paths (the polling mode no longer spawns a helper process per wait).
- Deprecated the
WaitForIt.V1macros; they now emit deprecation warnings pointing at the currentWaitForItAPI and will be removed in a future major version. - Modernized dependencies (
ex_doc,stream_data,credo).
2.1.0 - 2023--11-14
Changed
- Further improved documentation.
WaitForIt.case_wait/3will now raise aCaseClauseErroron timeout if there is noelseblock.WaitForIt.cond_wait/2will now raise aCondClauseErroron timeout if there is noelseblock.
2.0.0 - 2023-11-02
Changed
- Much improved documentation.
- Breaking change to return value of
WaitForIt.wait/2,WaitForIt.case_wait/3, andWaitForIt.cond_wait/2. - Rewrite of WaitForIt internals.
- Moved legacy code to
WaitForIt.V1.
1.4.0 - 2023-10-24
Added
- Add WaitForIt.wait! macro.
[1.3.0] - 2020-04-02
Changed
- Use DynamicSupervisor to manage condition variables.
[1.2.1] - 2019-03-14
Added
- Add
:pre_waitoption to all forms of waiting.
[1.2.0] - 2019-03-08
Added
- Add support for match clauses in
elseblock ofcase_wait. (Issue #9)
1.1.1 - 2018-03-03
Added
- Add idle timeout feature for ConditionVariable.
1.1.0 - 2017-09-02
Added
- Add support for
elseclause incase_waitandcond_wait. (Issue #4) - Add this CHANGELOG
Changed
- Use supervisor to manage condition variables. (Issue #5)
Fixed
- Grammar fixes for README and @moduledoc. Thanks to @GregMefford for the fixes.
- Fix unexpected messages from wait_for_it when used with Genserver
1.0.0 - 2017-08-28
- Initial release supporting
wait,case_wait, andcond_waitwith either polling or condition variable signaling.