
Security News
PHP Foundation Says Chainguard Broke Security Embargo Ahead of Emergency Patches
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.
@time-provider/core
Advanced tools
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 timings, swappable per
call site.
Note: Non baseline or additional features are available as addons.
flow), a host sleep (sleep) or a wall correction (snap), and step from timer to timer with moveTo("nextTimer")/moveUntil("noTimers").Date. Your code keeps working with the date type it already uses.Date, plain Moment.js are UTC-only - see ARCHITECTURE.md) - withTimezone(...) plus localNow()/utcNow().@time-provider/core.npm install @time-provider/core @time-provider/plugin-native
Swap plugin-native for plugin-dayjs, plugin-luxon, plugin-moment, plugin-moment-timezone, or plugin-temporal depending on the date library you use.
// 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.moveBy({ 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.moveBy({ seconds: 1 }); // 2nd attempt
timeProvider.clock.moveBy({ 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, monotonicNow, withTimezone
converter: IConverter<TDate>; // convertToUtc, convertToLocal
scheduler: IScheduler; // timers (once, every, recurring, wait), microtasks
timings: ITimings; // mark, measure, entries, clear
}
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
}
| 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 (
moveBy(),moveTo(),moveUntil(), 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.
Within the scope of this library, these two terms refer to different concepts.
Date and plain Moment.js are UTC-only.packages/<name>/README.md) for adapter-specific notes.FAQs
Inject time. Control it in tests.
The npm package @time-provider/core receives a total of 6 weekly downloads. As such, @time-provider/core popularity was classified as not popular.
We found that @time-provider/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.

Security News
PHP releases emergency patches for two critical RCE vulnerabilities as agencies urge organizations to take unpatched sites offline.

Threat Research
/Security News
A supply chain attack compromised LiteLLM 1.107.0 on PyPI and Docker, spreading malware that steals cloud credentials, developer secrets, and AI provider keys.