@marianmeres/ticker
v1.17.1
Published
[](https://www.npmjs.com/package/@marianmeres/ticker) [](https://jsr.io/@marianmeres/ticker)
Readme
@marianmeres/ticker
Do something when it ticks. With a store-compatible API.
Under the hood it uses recursive setTimeout with interval corrections, so it should
guarantee precise frequency except for edge cases where the synchronous
subscriber's work cannot keep up with the interval — it will never tick again before the
previous tick finishes.
Install
npm i @marianmeres/tickerdeno add jsr:@marianmeres/tickerBasic Usage
import { createTicker } from "@marianmeres/ticker";
// Create a ticker that ticks every 1000 milliseconds
const ticker = createTicker(1000);
// Subscribe to tick events
const unsub = ticker.subscribe((timestamp) => {
if (timestamp) {
// Do something on each tick
console.log("Tick at", timestamp);
} else {
// Ticker is stopped (or has not started yet)
console.log("Ticker stopped");
}
});
// Start the ticker
ticker.start();
// Later: stop and clean up
ticker.stop();
unsub();RAF Ticker
For animations or visual updates that should sync with the browser's repaint cycle, use
createTickerRAF:
import { createTickerRAF } from "@marianmeres/ticker";
const ticker = createTickerRAF(1000 / 60); // ~60fps
ticker.subscribe((timestamp) => {
if (timestamp) updateAnimation();
});
ticker.start();The RAF ticker uses requestAnimationFrame internally, with a polyfill for non-browser
environments. The minimum effective interval is ~16.67ms (60Hz).
Delayed Worker Ticker
Using a tick signal from a sync ticker for asynchronous work (e.g., periodic API
fetching)
may not be the best choice.
For such cases, use createDelayedWorkerTicker. Instead of guaranteeing frequency, it
guarantees a delay between worker calls:
import { createDelayedWorkerTicker } from "@marianmeres/ticker";
// Execute async work, then pause for 5 seconds, then repeat...
const ticker = createDelayedWorkerTicker(
async () => {
const response = await fetch("/api/data");
return response.json();
},
5000,
);
ticker.subscribe(({ started, finished, error, result }) => {
if (started && !finished) {
// Worker is in progress
console.log("Fetching...");
} else if (started && finished && !error) {
// Worker completed successfully
console.log("Got data:", result);
} else if (error) {
// Worker threw an error
console.error("Fetch failed:", error);
} else {
// Ticker is stopped (or has not started yet)
}
});
ticker.start();Calling stop() publishes a zeroed state (started: 0, finished: 0, error: null, result: null)
so subscribers always observe a clean "stopped" signal, mirroring createTicker's 0 emit on
stop. If a worker is mid-await when stop() is called, the in-flight result is discarded.
Dynamic Intervals
The interval can be a function that returns the number of milliseconds. This allows for dynamic timing:
// Exponential backoff example
let attempts = 0;
const ticker = createTicker((previousInterval) => {
return Math.min(1000 * Math.pow(2, attempts++), 30000);
});The function receives the previous interval value and the current store value as arguments:
type Interval = number | ((previous: number, storeVal: any) => number);Chaining API
All control methods return the ticker instance for chaining:
const unsub = createTicker(1000)
.setInterval(500)
.start()
.subscribe((v) => console.log(v));
// Later
ticker.stop();
unsub();Error Handling
Subscriber errors are caught and logged to prevent breaking the ticker. The ticker will continue running even if a subscriber throws:
ticker.subscribe((v) => {
if (v && someCondition) {
throw new Error("Oops!"); // Won't break the ticker
}
});You can provide a custom error handler using the options object signature:
const ticker = createTicker(1000, {
onError: (error) => {
// Custom error handling (or silence errors)
myLogger.warn("Subscriber error:", error);
},
});Behavior Notes
- Interval values are floored to integers. Floats like
100.9are accepted and treated as100.NaN,Infinity,-Infinity, and non-positive values throwTypeErrorat the point they are first observed (construction,setInterval(), or from an interval function). getInterval()is a pure read. It returns the most recently computed interval and does not re-invoke a function-form interval. Side-effectful interval functions (e.g. counters captured in closure) advance only on actual ticks.- Drift correction starts at the second tick. The first tick after
start()fires immediately; the configured interval is applied starting with the gap to the second tick. setInterval(newValue)takes effect on the next scheduled tick. The currently pending tick still uses the previously scheduled delay.getInterval()reflects the change immediately.
API Reference
See API.md for full API documentation.
Factory Functions
| Function | Description |
| --------------------------- | -------------------------------------------------- |
| createTicker | Fixed-frequency timer with drift correction |
| createTickerRAF | RAF-synchronized timer for animations |
| createDelayedWorkerTicker | Timer for async work with delay between executions |
Ticker Methods
All ticker instances provide: subscribe(), start(), stop(), toggle(),
isStarted(), setInterval(), getInterval().
Helper Exports
isBrowser(), getRaf(), setTimeoutRAF() — primarily for internal use.
