Skip to content

Instantly share code, notes, and snippets.

@Airblader
Last active November 29, 2023 16:33
Show Gist options
  • Save Airblader/75c9748ee276dcb25ae94d808740eab5 to your computer and use it in GitHub Desktop.
Save Airblader/75c9748ee276dcb25ae94d808740eab5 to your computer and use it in GitHub Desktop.
rxjs – Interpolate progress straight from hell
// Use https://rxviz.com to see it in action.
/**
* Interpolates a (numeric) source stream with predicted values based on how
* quickly the source stream's value increases. This was intended to smooth out
* a progress bar, but should one really use this piece of sheer terror in production?
*/
const interpolate = (source$, relax = 50) => {
/* This just allows a shorter syntax, avoiding return statements in arrow functions. */
const id = obj => obj;
/* Provides a stream of periodic events, relaxed by a given factor. */
const ticker$ = Rx.Observable.interval(0, Rx.Scheduler.animationFrame)
.windowCount(relax)
.switchMap(window => window.first());
/* Enriches the source stream with information of how much the value changed over which amount of time. */
const measuredSource$ = source$
.distinctUntilChanged()
.timeInterval()
.pairwise()
.map(([a, b]) => id({ change: b.value - a.value, value: b.value, duration: b.interval }))
/* Might as well guess ¯\_(ツ)_/¯ */
.startWith({ duration: 1000, change: 1, value: 0 });
return ticker$
/* Interpolate source stream with additional emissions and remember at what time it was emitted. */
.withLatestFrom(measuredSource$, (_, measured) => id({ ...measured, index: 0 }))
.timeInterval()
.map(timed => id({ ...timed.value, tick: timed.interval }))
/* Ensure that index resets everytime the source observable emits a new value. */
.scan(
(acc, value) => id({ ...value, index: (value.value === acc.value) ? acc.index + 1 : 0 }),
{ duration: 0, change: 0, value: 0, index: 0, tick: 0 })
/* Predict the progress based on the source's last value and everything we know. */
.map(({ value, duration, index, change, tick }) => value + tick * index * change / duration);
};
/* A fake source stream. */
const source$ = Rx.Observable.timer(0, 2500)
.take(10)
.map(x => 10 * x);
/* Our prediction stream. Rounded for glory. */
const interpolated$ = interpolate(source$, 50)
.map(value => Math.floor(value));
/* Put a fake key on the source and prediction stream to use groupBy. There's almost certainly
a better way, but after everything above I'm just not going to bother. */
const kSource$ = source$.map(x => [0, x]);
const kInterpolated$ = interpolated$.map(x => [1, x]);
/* Graph source versus prediction. */
kSource$
.merge(kInterpolated$)
.groupBy(([i, x]) => i % 2 === 0)
.map(group => group.map(([_, value]) => value))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment