rxjs-exhaustmap-with-trailing
A tiny RxJS operator that extends exhaustMap to also emit the trailing value from the source observable.
Repository Health
Technical Analysis
rxjs-exhaustmap-with-trailing is a focused RxJS operator library that solves a specific gap in RxJS’s built-in flattening operators: exhaustMap() ignores every source emission that arrives while a projected inner observable is still active, which means the very last user action or state change before completion is silently dropped. This package adds exhaustMapWithTrailing() and exhaustMapToWithTrailing(), drop-in variants that preserve exhaustMap’s ignore-while-busy semantics but also fire one final emission using the most recent value received before the inner observable completes.
Internally the operator combines RxJS’s throttle() (with both leading and trailing emissions enabled) and exhaustMap(), releasing the throttle window only when the previous inner observable finishes via finalize(). The implementation is a single TypeScript file, adapted from a community solution originally posted on the RxJS GitHub issue tracker, and is exercised by a marble-diagram test suite using RxJS’s TestScheduler. It’s aimed at UI patterns like button-mashing prevention or save-on-idle flows where exhaustMap’s default behavior would otherwise mean a user’s last click or edit never triggers a follow-up action.
What You Get
- exhaustMapWithTrailing() - a drop-in operator that behaves like exhaustMap but also emits the last value that arrived while the inner observable was active
- exhaustMapToWithTrailing() - a convenience wrapper around exhaustMapWithTrailing for mapping to a fixed inner observable instead of a projection function
- Full TypeScript typings - generic OperatorFunction<T, R> signatures that match RxJS’s own operator typing conventions
- Marble-diagram test suite - a Jest + RxJS TestScheduler test file documenting the exact timing semantics for edge cases like empty sources, errors, and same-frame trailing emissions
Common Use Cases
- Preventing double-submit while capturing the final edit - guard a save button against rapid clicks with exhaustMap-like behavior, but still fire once more for the user’s last edit
- Debounced polling triggered by user interaction - ignore intermediate triggers while a request is in flight, but always follow up with the most recent trigger
- Autosave on idle - fire a save request per burst of changes while guaranteeing the final change in a burst is not lost
Under The Hood
Architecture The library is a single TypeScript module (index.ts) that composes RxJS primitives rather than reimplementing flattening logic from scratch: exhaustMapWithTrailing wraps the source in defer(), threads it through throttle() configured with {leading: true, trailing: true} gated by a release Subject, then applies exhaustMap() to project each throttled value into an inner observable scheduled on asyncScheduler; a finalize() callback on the inner observable calls release.next() to reopen the throttle window once the inner observable completes. exhaustMapToWithTrailing is a one-line wrapper around exhaustMapWithTrailing for the common case of mapping to a fixed inner observable. There is exactly one behavioral seam - the release Subject controlling the throttle gate - so introducing a wrong reopening condition there is the only way the operator’s guarantees would break.
Tech Stack The package is written in TypeScript and has a single peer dependency, rxjs ^7.x, with no runtime dependencies of its own. Build tooling produces separate CommonJS and ES module outputs (dist/cjs, dist/esm) via two parallel tsc invocations orchestrated with npm-run-all, and releases are cut with standard-version. Testing runs on Jest with ts-jest, and formatting is enforced by Prettier via .prettierrc. There is no CI configuration checked into the repository.
Code Quality Test coverage is thorough for the operator’s core guarantees: the test suite uses RxJS’s TestScheduler and marble-diagram syntax to assert exact timing behavior across scenarios including empty source, leading+trailing emissions, trailing-after-source-completion, immediate completion while waiting on the inner observable, same-frame trailing values, error propagation, and the fixed-observable variant, plus one async integration test using real timers. Both exported functions are fully typed with the same generic signature RxJS itself uses, and carry JSDoc comments describing parameters and behavior. No CI workflow enforces these tests on push, and there is no linter configuration beyond Prettier, so quality here depends on manual test runs before release.
API Design The public API is deliberately minimal - two functions, a drop-in exhaustMap replacement and a drop-in exhaustMapTo replacement - so adopting it requires no more boilerplate than swapping an existing RxJS operator import. Naming closely mirrors RxJS’s own operator conventions, which keeps the learning curve close to zero for anyone already using exhaustMap or throttle. The tradeoff is a narrow, single-purpose surface: it solves exactly one gap in RxJS’s flattening operators and credits the original community-sourced solution rather than presenting a novel algorithm.