gohypergiant / standard-toolkit

The web "standard library" for the Accelint family of systems.
Apache License 2.0
0 stars 0 forks source link

feat: timers synced to clock #56

Open brandonjpierce opened 1 week ago

brandonjpierce commented 1 week ago

This PR allows us to kill two birds with one stone:

  1. Have timers consistently fire across multiple tabs and application instances
  2. Remove the need for the harmonic interval package which is a bit of a pain to test
setClockInterval(() => console.log('hi'), 1000);
// will start logging hi every second after the next clock second ticks

setClockTimeout(() => console.log('hi'), 1000);
// will log hi after one second after the next clock second ticks

---

setClockInterval(() => console.log('hi'), 250);
// clock second tick -- hi
// 250ms hi
// 250ms hi
// 250ms hi
// clock second tick -- hi

The timers work by waiting until the next second based on system clock plus or minus a millisecond or two due to new Date() inaccuracies. From there it will fire your callback based on the supplied frequency value in milliseconds (same API as setTimeout/setInterval).


This also replaces harmonic interval since timers are now going to always fire roughly within the same timeframe given the same ms value e.g.

setClockInterval(() => console.log('hi'), 250);
setClockInterval(() => console.log('hi'), 250);
setClockInterval(() => console.log('hi'), 250);
setClockInterval(() => console.log('hi'), 250);
setClockInterval(() => console.log('hi'), 250);

Each of these could be on different pages, in a web worker, etc and they would still fire at the "same time" since the execution begins on the next clock tick. This sort of removes the need for the harmonic interval bucketing trick entirely.


Note 1: I opted for a recursive setTimeout instead of calling a setInterval for the setClockInterval implementation. setInterval delivers its function to the callstack regularly regardless of the status of its previous function calls, whereas setTimeout will schedule its next call only when its function runs and the new setTimeout is scheduled. So if setInterval was timed to deliver function calls every 1000ms, and the execution of that function takes 300ms, the actual interval between the end of the call and the next invocation would actually be 700ms. If there is any variance in the duration of that execution, that interval will also change, and if the execution takes more time than the interval, you can end up with multiple calls queued back to back. Compare this to a recursive setTimeout, where the next scheduled call will always be delivered after the exact time specified by the previous setTimeout.

Note 2: since we are not dealing with a high frequency interval use case that needs to have consistent callback execution, like a metronome, I opted to skip any fancy optimizations as described here. Additionally, the 1-10ms drift that can occur in a setTimeout or setInterval is negligible for 99% of our use cases.