# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.7.2] - 2026-08-20
### Added
- **Every spawned task now carries its logwise context with it** - Spawning captures the current facade context, derives a child token, and stores it in the spawned task rather than leaving durable identity in thread-local state. Every poll enters that token and restores the previous thread or worker context on return or unwind, so parentage survives migration. Optional `logwise-forensic` events cover spawn, first poll, wake, completion, cancellation, and drop; `logwise-performance` separately measures wall lifetime, active poll time, and wake-to-poll latency; and `logwise-diagnostic` admits the local task label as an on-demand detail field. These features add no runtime or executor-integration dependency: without an installed dispatcher the facade remains a no-op.
- **`block_on`, so a program can cross from sync into async without naming a backend** - No trait in this crate could drive a future to completion from synchronous code. `SomeExecutor` could only *spawn*, and the observer it hands back is itself a future, so consuming a result from `fn main` meant already being inside some other executor. Every program therefore bottomed out in a backend-specific entry point - `tokio::runtime::Runtime::block_on`, `test_executors::spin_on` - which is the coupling this crate exists to remove.
Three things land together. The free `some_executor::block_on` polls a future in place on the calling thread, parking between polls; it involves no executor at all, so unlike spawning it imposes neither `Send` nor `'static` and the future may borrow from the caller's stack. `SomeExecutor::block_on` is the executor-dispatched form, and `SomeExecutor::block_on_objsafe` is the type-erased method that carries the behavior - the typed one erases, calls it, and downcasts, which is the same shape as `spawn`/`spawn_objsafe`. Both are *provided* methods defaulting to the free function, so nothing downstream breaks and every existing implementor gains them.
They live on `SomeExecutor` rather than on `SomeExecutorExt` deliberately. Object safety was never the obstacle - the trait already carries generic methods behind `where Self: Sized` - and `current_executor()` and `global_executor()` hand out a `Box<DynExecutor>`, so putting `block_on` on the non-objsafe descendant would have made it unreachable from exactly the two places most callers get an executor. `Box<DynExecutor>` forwards `block_on_objsafe` to the executor inside it, so blocking on a boxed executor reaches that executor's own implementation.
The `F::Output: Send + 'static` bound on the typed method is the price of that erasure, and applies to the *output* only, never to the future. Callers who need a non-`Send` output can use the free function and forgo executor dispatch.
- **Local and static executors can opt into blocking without inheriting a deadlock** - `LocalBlockOn::block_on_local` and `StaticBlockOn::block_on_static` are required methods with no parking default. A backend that owns the calling thread can implement its real "drive my scheduler until this future resolves" loop; a browser-main-thread backend, where blocking is impossible, implements nothing. Both accept borrowed, non-`Send` futures and non-`Send` outputs, and the notifier-erasing adapters preserve the capability when their underlying executor provides it.
- **`ExecutorMain`, an executor-agnostic program entry point** - An `#[some_executor::main(SomeBackend)]` attribute cannot be written against an inherent method, because the macro has never heard of the backend it is handed; the call has to go through a trait this crate defines. `ExecutorMain::main` is that trait: construct the chosen executor, install it, run this future. `entry_point::run_main` packages the ordinary blocking implementation, so a backend that can block writes a one-line body.
It returns `()` rather than the future's output, which is what makes it implementable on the wasm32 main thread: there, `fn main` returns while the program keeps running on the event loop, so there is no moment at which a result could be produced. A fallible `async fn main` handles its `Result` inside the future. The macro itself is not part of this release.
### Fixed
- **A `block_on` override can now scope the thread executor the way the default does** - `SomeExecutor::block_on`'s contract says the executor is current for the duration of the block and no longer, and the default honored it through a crate-private `replace_thread_executor`. A backend overriding `block_on_objsafe` - which the docs say some backends *must* - had only the one-way `set_thread_executor`, so it could not keep the same promise. Rolling it by hand cost a `clone_box` of the displaced executor, and worse, could not restore the *empty* state: a thread with no executor before the call silently kept one afterwards, and `current_executor()` there began answering differently. There was no workaround.
`thread_executor::install_thread_executor` returns a `ThreadExecutorGuard` that restores the previous executor - including none - when dropped, on unwind as well as on return. It is the mechanism the default now uses, rather than a parallel one.
- **`block_on`'s override guidance named only one of the two reasons to override** - It said an executor that runs tasks on the calling thread must override `block_on_objsafe`, which is necessary but not sufficient, and reads as "the default is fine for you" to a backend for which it is not. A tokio multi-thread runtime satisfies the stated condition, since its workers run elsewhere; but the default polls the future on the calling thread, and that thread has not entered the runtime, so the first `tokio::time::sleep` or `TcpStream` inside it panics. The stall detector does not help - that is an immediate panic, not a hang. The notes now give both conditions: who makes progress, and what context the polling thread is in.
- **The Quick start doctest no longer hangs on wasm32** - It spawned on `current_executor()` and awaited the observer inside `wasm_lite_std::block_on`, which on the browser main thread is a deadlock rather than a slow test. `block_on` there polls in a loop whose `yield_now` is a no-op (`atomics_wait_timeout_ms_try` returns `Unsupported` off a worker), so it never returns control to the JavaScript event loop - while the executor it is waiting on is the builtin fallback, whose wasm32 spawn path starts a web *worker*, and worker startup is delivered by exactly the event loop being starved. The doctest ran to the harness timeout on every wasm32 run.
It now uses `wasm_lite_std::async_doctest!`, which drives the future on the event loop on wasm32 and blocks natively, so the crate's headline example is verified on both targets instead of hanging on one. Note that moving the fallback executor to `spawn_local` would *not* have fixed this: the task would land on the calling thread's event loop, which a blocked main thread is equally unable to turn. Nothing can block on the browser main thread; the example had to stop trying.
The `block_on` examples added above take the other route, `wasm_lite_std::worker_doctest!`, which runs them on a worker where blocking is legal - so they are exercised on wasm32 too rather than compiled out.
### Changed
- **`block_on` reports the parked-forever mistake instead of hanging** - The default `SomeExecutor::block_on` parks the calling thread, which is correct for an executor whose tasks run on other threads and a deadlock for one that runs them on the caller's. The failure is silent and indistinguishable from a slow program, so the parking loop watches for it: a thread that waits past a threshold without a *single* wakeup gets a diagnostic naming the likely cause and the fix. It measures time since the last wake rather than total elapsed time, so a future that is merely waiting a long time on something real stays quiet.
`SOME_EXECUTOR_BUILTIN_SHOULD_PANIC=1` escalates the diagnostic to a panic, the same switch the builtin executors already use. `SOME_EXECUTOR_BLOCK_ON_STALL_SECS` sets the threshold (default 10) or disables the check with `0`.
On the wasm32 main thread `block_on` panics immediately rather than watching anything. Blocking cannot work there under any implementation: the JavaScript event loop delivers every wakeup - timers, promises, `fetch`, worker messages - and it only runs once the stack unwinds, so a blocked main thread starves the mechanism that would end the block. `ExecutorMain` is the portable option, and `block_on` works normally on a worker.
- **`SpawnedStaticTask` implements `Future`, and its `poll` is public** - `SpawnedStaticTask::poll` was `pub(crate)` and the type had no `Future` impl, so a downstream `SomeStaticExecutor` had no correct way to drive the task it was handed. The reachable workaround, `spawned.into_future().await`, drops the wrapper and with it the task's `ObserverSender` - the future still runs, but its observer reports `Cancelled` and never sees the value. `SpawnedStaticTask` now implements `Future` (polling with no executor context, mirroring `SpawnedTask`), and `poll` is `pub` so an executor that *can* supply a static or Send executor context can populate `TASK_STATIC_EXECUTOR` and `TASK_EXECUTOR` for the polled future. Both changes are additive.
The `into_future` docs on all three spawned task types now say what dropping the wrapper costs, and point at the polling API instead.
## [0.7.1]
### Changed
- **The test suite dropped `test_executors`, breaking a publish cycle** - `test_executors` depends on this crate, so a dev-dependency pointing back at it formed a cycle that neither crate could publish out of; the `>=0.4.1, <0.6` range and the paragraph of manifest comment explaining it existed only to work around that. The two things the tests actually wanted from it are now supplied by wasm_lite, which this crate already depends on. `spin_on` becomes either `wasm_lite_std::block_on` (native-only tests, including the two that need `catch_unwind` around a driven future) or a plain `.await` in a test that is now an `async fn`; `#[test_executors::async_test]` becomes `#[wasm_lite::wasm_lite_test]`, which is what it expanded to on wasm32 anyway. Tests converted to `async fn` are driven by the browser event loop on wasm32 and by `block_on` off it, and the four that were `cfg_attr` pairs collapse to one attribute. `block_on` parks on a condvar with a real waker where `spin_on` busy-polled, so the native suite no longer burns a core per blocked test.
No library code changed, and `test_executors` remains a recommended reference executor in the crate docs - this is about what *this* crate's own tests link against.
- **Three `no_run` doctests now actually run** - The `TypedObserver` examples for cancellation-on-drop, `observe()` and `detach()` used `todo!()` as a stand-in for an observer, so they only ever type-checked. They now obtain a real observer by spawning on `current_executor()`, which is public, always available, and works in a browser. The setup is hidden behind `#`, so the rendered examples are unchanged - they are simply verified now, on both native and wasm32.
The remaining three `no_run` examples stay that way, and now record why in a form that answers the question rather than restating it: the two in `global_executor` cannot run because `set_global_executor` writes a process-wide `OnceLock` and edition 2024 merges a crate's doctests into a single process, so a running version would set the global executor for every other doctest and change what the neighbouring `is_some()` examples observe; the `SpawnedLocalTask` one cannot run because polling it needs a value of the concrete `Executor` type parameter and this crate exposes no public `SomeLocalExecutor`. The one `compile_fail` block is a deliberate negative test.
- **The wasm_lite dev-dependency comes from crates.io again** - The two features the tests above rely on - a public `wasm_lite_std::block_on`, and `#[wasm_lite_test]` accepting an `async fn` and registering natively - were committed upstream but unreleased when those tests landed, so the manifest carried a `[patch.crates-io]` block pointing at a sibling checkout. `cargo publish` ignores `[patch]` and resolves dev-dependencies from the registry, so that block blocked publishing. wasm_lite 0.1.2 has shipped: the dev-dependency now requires `0.1.2` and the patch block is gone. The library half still only needs `0.1.1`, and its requirements are unchanged.
## [0.7.0]
### Changed
- **`wasm_lite` and `wasm_lite_std` now come from crates.io** - Both were local path dependencies on a sibling checkout, which cannot be published and left the crate unresolvable for anyone without that checkout. They're pinned to the published `0.1.1` instead.
### Fixed
- **The wasm32 test suite runs again** - Three separate breakages: `scripts/wasm32/tests` invoked a `runner` binary that wasm_lite no longer builds (the runner is the `wasm_lite` CLI's `run` subcommand); the wasm32 link flags never exported `__stack_pointer`, which wasm_lite's worker bootstrap requires of any module that spawns threads; and the registry `wasm_lite` linked alongside the path copy reached through the dev-cycle patches, producing duplicate symbols at link time. The full suite now passes in a browser.
- **Erased notifiers receive the value, not the box around it** - Any task carrying a notifier panicked with "Downcast failed" the moment it completed, on every type-erased spawn path: `Box<dyn SomeExecutor>::spawn` (which is what `current_executor()` hands you), `spawn_objsafe`, `spawn_static`, `spawn_static_objsafe`, and the local objsafe path. The boxed notifier's "unboxing" impl forwarded `&Box<dyn Any>` unchanged, and Rust resolves that to `&dyn Any` by unsizing the *box* rather than dereferencing it - so the erased notifier tried to downcast a `Box<dyn Any>` to the task's output type and always failed. The panic fired inside `send`, before the result was published, which meant a task that ran perfectly well came back to its observer as `Cancelled`.
- **Successful tasks no longer report themselves cancelled** - `ObserverSender::drop` marked the in-flight cancellation token unconditionally, including when the task had already produced its value. Anything holding a clone of that token - `IS_CANCELLED.with(|c| c.cloned())` is public API - saw `is_cancelled()` come back `true` after a clean completion. Only a task dropped before it produced a value marks the token now.
- **Completed observers stay completed** - Consuming a successful observer with `observe()` or `.await` no longer sends a late cancellation request to the executor when the observer is dropped.
- **Observer wakeups no longer risk re-entrant deadlock** - Completion and cancellation now release the observer state mutex before invoking its waker, so eager executors can safely poll again inline.
- **Re-entrant static spawns keep task-locals private** - The native static fallback now waits for active task-local scopes to restore their values before polling a child task, preventing request context or credentials from leaking across task boundaries.
- **WASM tests use the runner that owns their test format** - The test script now invokes the wasm_lite runner instead of silently handing wasm_lite test sections to `wasm-bindgen-test-runner`; a missing runner fails fast with setup guidance.
- **Missing task executors stay missing** - Tasks polled without an executor now install an explicit empty task-local context instead of inheriting a re-entrant parent task's executor.
- **Task-locals now survive panics** - If a future panicked during a poll, both `task_local!` scopes and the built-in task-locals (`TASK_ID`, `TASK_LABEL`, `IS_CANCELLED`, ...) leaked the panicking task's values onto the OS thread, where unrelated tasks could observe them (and later `scope()` calls would panic). Restoration now happens in drop guards, so unwinding cleans up properly.
- **Nested `scope()` of the same task-local works** - `KEY.scope(a, async { KEY.scope(b, ...).await }).await` previously panicked with "Task-local already set"; it now shadows and restores the outer value, as the docs always promised.
- **Lost-wakeup fix in `LastResortExecutor`** - The waker could fire in the window between the executor deciding to sleep and actually parking on the condvar, hanging the task forever. The static path got this fix in d274a3b; the non-static path now matches, plus proper acquire/release ordering on the wake handshake for weakly-ordered targets.
- **`into_objsafe` preserves task identity** - Converting a task for type-erased spawning (including implicitly via `Box<dyn SomeExecutor>::spawn`) silently allocated a fresh `TaskID`, breaking correlation with the ID recorded at creation. The ID (and thus `TASK_ID` inside the future) is now preserved.
- **`detach()` no longer leaks** - The default `Observer::detach` was `mem::forget`, permanently leaking the observation channel (and the task's eventual result) for erased observers - every `spawn(...).detach()` through a boxed executor leaked an allocation. A new object-safe `Observer::detach_in_place(&mut self)` primitive detaches without leaking. **Breaking**: implementors of `Observer` must now provide `detach_in_place`.
- **`Task::spawn_local_current` is implemented** - It was a documented public method whose body was `todo!()`; it now spawns on the thread-local executor (and panics with a helpful message if none is set).
- **Last-resort executors respect `poll_after`** - Spawning a task configured with a future `poll_after` on the fallback executors used to trip an assertion; they now delay (sleeping on native, `setTimeout` on wasm) until the configured time.
- **`pin_static_to_thread` keeps the task's notifier** - The notifier was silently dropped, which reads as a spurious cancellation signal under the documented drop-without-notify contract; it's now notified on completion. `pin_static_to_thread` and `Task::pin_current` gained an `ObserverNotified` bound on the notifier parameter to support this.
- **wasm `close()` interception is reusable** - After a `close()` attempt in a context where close doesn't terminate execution (browser windows), the saved original handler was lost and later patches chained the wrapper to itself. State is now restored around the call.
- **Awaiting an observer after `observe()` took the value** panics with a clear message instead of `unreachable!`, and the behavior is documented.
- Misc: `thread_static_executor` no longer holds the thread-local borrow while running the last-resort fallback (which could panic re-entrantly); `Box<dyn SomeStaticExecutor>::spawn_static_async` now routes through the underlying executor's async spawn path; `common_poll` no longer calls user `clone_box` while holding a task-local borrow; `task_local!` expands with fully-qualified `::std` paths; docs corrected where they promised `TASK_LOCAL_EXECUTOR` population and synchronous wasm spawning that don't happen.
## [0.6.3]
### Added
- New project scripts for consolidated checks, docs, clippy, and tests across native and wasm targets.
### Changed
- Switched core synchronization paths to `wasm_safe_thread` for wasm-friendly behavior.
- Updated CI and wasm32 documentation/build flags for more reliable browser-target checks.
- Improved last-resort executor behavior on wasm to avoid blocking as aggressively.
- Documentation pipelines now skip dependency docs (`--no-deps`) to keep crate docs focused and avoid external rustdoc failures.
## [0.6.2]
### Added
- **Panic support for builtin executors** - You can now set `SOME_EXECUTOR_BUILTIN_SHOULD_PANIC=1` to make the builtin executor panic on task failures. Perfect for debugging when you want your executor to tell you loudly that something went wrong, rather than silently carrying on.
- **Improved WASM32 build configuration** - Fresh cargo config flags for wasm32 targets that make cross-compilation smoother. Your WASM builds just got a little friendlier.
### Changed
- **Documentation refresh** - Clearer docs to help you get up and running faster. We're not saying the old docs were confusing, but... let's just say these are better. Plus some fresh README updates so you know what you're getting into.
- **CI pipeline updates** - Housekeeping on the CI front to keep things running smoothly. Nothing you'll notice, but our build robots are happier.
### Fixed
- **WASM-bindgen compatibility** - Squashed a pesky issue ([wasm-bindgen#4820](https://github.com/wasm-bindgen/wasm-bindgen/issues/4820)) that was making WASM builds grumpy. Your WebAssembly projects should now compile without the drama.
### Housekeeping
- Added `.gitignore` for cleaner repos
- Updated Cargo metadata
- Bumped `test-executors` dev dependency to latest
- Clippy made us tidy up some code—you won't see the difference, but it's there, judging us silently
## [0.6.1] - Previous Release
*Initial tracked release for changelog purposes.*
[Unreleased]: https://github.com/drewcrawford/some_executor/compare/v0.7.1...HEAD
[0.7.1]: https://github.com/drewcrawford/some_executor/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/drewcrawford/some_executor/compare/v0.6.3...v0.7.0
[0.6.3]: https://github.com/drewcrawford/some_executor/compare/v0.6.2...v0.6.3
[0.6.2]: https://github.com/drewcrawford/some_executor/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/drewcrawford/some_executor/releases/tag/v0.6.1