@time-provider/core
v3.0.1
Published
Time-Provider : Core library ~ Your single time interface for all your JavaScript / TypeScript projects.
Maintainers
Readme
Time-Provider
Disclaimer
Time-Provider is currently under active development. While the project follows Semantic Versioning and may release versions beyond 1.0 before the API is considered stable, the API should be considered unstable for the time being and may change between releases.
Note: As the new API (timers: once/every/recurring/wait with abort/dispose interfaces) is released, a compat addon that still exposes the previous low-level-like methods (setTimeout/clearTimeout) is also available here. It will expose your previous method calls through the compat prefix and may help transitioning to the new API.
This disclaimer will be removed once the API has stabilized.
Time is a dependency
Code coupled to the native Date object, Temporal objects, any specific date library, or the
environment's timers has an implicit dependency on the system clock. That's what pushes teams toward global
fake-timer libraries for testing - patching Date/timers process-wide,
which affects unrelated code and makes tests harder to reason about.
vs. jest.useFakeTimers() / sinon.useFakeTimers(): scoped per call site, no global patch, no restore/cleanup step.
time-provider makes time an explicit, injectable dependency instead: a
single object exposing a clock, a converter, a scheduler, and a performance API swappable per
call site.
Note: The animation-frame API is available as an addon.
Features
- Four clock strategies: system (real time), fixed, manual (advance time explicitly), sequential (predefined instants) - same API for production and tests.
- Deterministic timers: driven by the clock strategy, not the real event loop, so manual/sequential/fixed runs are synchronous and don't depend on wall-clock time.
- Bring your own date library: adapters for Temporal, Day.js, Luxon, Moment.js, Moment.js + moment-timezone, and native
Date. Your code keeps working with the date type it already uses. - Real timezone support where the underlying library allows it (native
Date, plain Moment.js are UTC-only - see ARCHITECTURE.md) -withTimezone(...)pluslocalNow()/utcNow(). - Tree-shakable: no deterministic runtimes bundled when not imported.
- Zero runtime dependencies in
@time-provider/core.
Install
npm install @time-provider/core @time-provider/plugin-nativeSwap plugin-native for plugin-dayjs, plugin-luxon, plugin-moment, plugin-moment-timezone, or plugin-temporal depending on the date library you use.
Usage
// production: import the default production runtime
import { createTimeProvider } from "@time-provider/core";
import { plugin } from "@time-provider/plugin-native";
// create a production runtime
const timeProvider = createTimeProvider.for(plugin).create();
class UserService {
constructor(private readonly timeProvider: ITimeProvider<Date>) {}
createUser() {
return { createdAt: this.timeProvider.clock.utcNow() };
}
}// test: import the deterministic runtime
import { createTimeProvider } from "@time-provider/core/deterministic";
import { plugin } from "@time-provider/plugin-native/deterministic";
// create a deterministic runtime
{
using timeProvider = createTimeProvider
.for(plugin)
.asManual()
.withInitialTime("2026-01-01T00:00:00.000Z")
.create();
let retries = 0;
{
//timers are cleared when disposing their handles
using timerHandle = timeProvider.scheduler.timers.every({ seconds: 1 }, () => retries++);
timeProvider.clock.advance({ seconds: 3 });
}
expect(retries).toBe(3);
}Same idea, closer to a real service - a retry/backoff loop injected with the time provider, unaware of which strategy backs it:
// production: a retry/backoff service, injected with the real time provider
class RetryingOperation {
constructor(private readonly timeProvider: ITimeProvider<Date>) {}
run(operation: () => boolean, onGiveUp: () => void, maxAttempts = 3) {
let attempt = 0;
this.timeProvider.scheduler.timers.recurring(() => {
attempt++;
if (operation()) return false; // succeeded, stop retrying
if (attempt >= maxAttempts) {
onGiveUp();
return false;
}
return { seconds: attempt }; // back off: 1s, 2s, 3s...
});
}
}
new RetryingOperation(timeProvider).run(sendRequest, pageOnCallEngineer);// test: same service, injected with a manual provider instead - no real waiting
using timeProvider = createTimeProvider.for(plugin).asManual().withInitialTime(0).create();
let attempts = 0;
let gaveUp = false;
new RetryingOperation(timeProvider).run(
() => ++attempts === 3, // succeeds on the 3rd try
() => (gaveUp = true),
);
timeProvider.clock.advance({ seconds: 1 }); // 2nd attempt
timeProvider.clock.advance({ seconds: 2 }); // 3rd attempt, succeeds
expect(attempts).toBe(3);
expect(gaveUp).toBe(false);Every time provider exposes the same four-part surface:
interface ITimeProvider<TDate> extends IHasAbortSignal, IDisposable {
clock: IClock<TDate>; // localNow, utcNow, timestampNow, withTimezone
converter: IConverter<TDate>; // convertToUtc, convertToLocal
scheduler: IScheduler; // timers (once, every, recurring, wait), microtasks
performance: IPerformance; //now, getEntries, measure,...
}scheduler is where everything that schedules a callback to run later sits, so the
scheduling addons extend that facet rather than the root. Animation-Frame comes with
its addon that
extends ITimeProvider with:
interface ITimeProvider<TDate> {
scheduler: { animation: IAnimationFrameApi }; //scheduleFrame
}Clock strategies
| Strategy | Behavior | Typical use | | ---------- | ------------------------------------- | -------------------------------------- | | System | Real time, real timers | Production | | Fixed | Always the same instant | Deterministic single-instant tests | | Manual | Advances only when told to | Simulations, timer/retry logic tests | | Sequential | Returns a predefined instant sequence | Tests asserting on changing timestamps |
createTimeProvider.for(plugin).asFixed().withFixedTime("2026-01-01T00:00Z").create();
createTimeProvider.for(plugin).asManual().withInitialTime("2026-01-01T00:00Z").create();
createTimeProvider
.for(plugin)
.asSequential()
.withSequentialTime("2026-01-01T00:01Z")
.withSequentialTime("2026-01-01T00:02Z")
.create();Manual and sequential clocks run synchronously. A due timer callback fires in-line, as a direct side effect of the call that made it due (
advance()(orlocalNow(),utcNow()on sequential clocks)) - not on a real event-loop tick. This is what makes them deterministic withoutawait, but it means call ordering can differ subtly from a real async run. UsetimestampNow()instead when you only need a value to compute with - it never triggers any timer on sequential clocks or advances time.
Addons vs. Plugins
Within the scope of this library, these two terms refer to different concepts.
- A plugin, is essentially an adapter. It allows you to connect your preferred date library (e.g. Luxon, Temporal, etc.) to the Time Provider core library without adding any new functionality. Its sole purpose is to bridge the two libraries (e.g. the dayjs plugin).
- An addon, as the name suggests, extends the library by introducing new functionality or enhancing existing facades (e.g. the animation-frame API addon)
Available addons
- Animation-frame API addon - access browser-specific animation frame timers
- Compat addon - keep calling native-style setTimeout/setInterval/queueMicrotask/performance while you migrate
- Cron addon - schedule recurring callbacks with the cron syntax or a JSON-friendlier one
- ETA addon - get the ETA (estimated time of arrival) for a task by notifying its progression
- Idle addon - run callbacks when the host reports itself idle, drained on demand on a deterministic clock
Learn more
- Guide - Read the guide
- API - Browse the library API
- ARCHITECTURE.md - how the packages fit together, the plugin/adapter model, why native
Dateand plain Moment.js are UTC-only. - CONTRIBUTING.md - development setup, workflow, reporting bugs/features.
- BENCHMARK.md - faster than jest/sinon fake timers.
- Per-package README (
packages/<name>/README.md) for adapter-specific notes. - CHANGELOG.md - changes log from the core library and all plugins.
