Compatibility Layer
@time-provider/addon-compat adds a .compat facade exposing native-style setTimeout/setInterval/queueMicrotask call signatures on top of a Time-Provider's own timers, plus a performance object with the members of the performance global. Each global keeps its name under .compat, so performance.now() becomes timeProvider.compat.performance.now(). Like every addon it composes in with .use(addon) and ships two entry points — one for a system Time-Provider, one for a deterministic one:
import { createTimeProvider } from "@time-provider/core";
import { plugin } from "@time-provider/plugin-native";
import { addon } from "@time-provider/addon-compat";
const timeProvider = createTimeProvider.for(plugin).use(addon).create();
const handle = timeProvider.compat.setTimeout(() => console.log("tick"), 500);
timeProvider.compat.clearTimeout(handle);On a deterministic Time-Provider the same calls run against that runtime's own simulated clock, so code written against this facade is testable the same way as code written against .scheduler:
import { createTimeProvider } from "@time-provider/core/deterministic";
import { plugin } from "@time-provider/plugin-native/deterministic";
import { addon } from "@time-provider/addon-compat/deterministic";
const manual = createTimeProvider.for(plugin).use(addon).asManual().withInitialTime(0).create();
let ticks = 0;
manual.compat.setInterval(() => ticks++, 1000);
manual.clock.moveBy({ seconds: 3 });
console.log(ticks); // 3Why this facade exists
.scheduler (once/every/recurring/wait) is the library's own API — see Deterministic Timers. This addon exists for the codebase that already calls setTimeout/setInterval/clearTimeout/etc. directly and wants to migrate onto a Time-Provider incrementally: swap the call site for timeProvider.compat.setTimeout(...), without rewriting it to the once/dispose() shape first. Under the hood every method delegates straight to .scheduler, so it follows the exact same clock strategy rules — real timers on a system clock, synchronous and in-line on manual/sequential, never firing on a fixed clock.
See Migration for the full path, from compat to an injected Time-Provider.
The methods
| Method | Delegates to | Cancelled by |
|---|---|---|
setTimeout(callback, delayMs?, ...args) | scheduler.timers.once | clearTimeout(handle) |
setInterval(callback, delayMs?, ...args) | scheduler.timers.every | clearInterval(handle) |
queueMicrotask(callback) | scheduler.microtasks.queue | nothing — it always runs |
delayMs defaults to 0 when omitted or negative, matching .scheduler. Any args after it are passed to the callback, as the native functions do. Each clear* method is a no-op if the handle's callback already ran or was already cleared — it just calls .dispose() on the IScheduledHandle the matching set* method returned.
There is deliberately no setRecurring here: nothing native ever had that signature, so there is no call site to migrate. Self-rescheduling callbacks go through scheduler.timers.recurring.
The performance members
now(), timeOrigin, getEntries(), getEntriesByName(), getEntriesByType(), mark(), measure(), clearMarks() and clearMeasures() sit on .compat.performance, with the same signatures as the performance global, measure(name, startMark?, endMark?) included:
timeProvider.compat.performance.mark("request-start");
// ...
timeProvider.compat.performance.measure("request", "request-start");
console.log(timeProvider.compat.performance.getEntriesByName("request")[0]?.duration);They map onto the Time-Provider's own clock.monotonicNow(), clock.monotonicOrigin and timings, so on a deterministic Time-Provider they read that runtime's simulated timeline — the point being that a file calling performance.mark(...) and setTimeout(...) side by side has one object to swap them both for. On a system Time-Provider, the getEntries* readers list the host's whole timeline, including entries it records itself (resource, navigation...), as the performance global does.
What other addons add here
Composing addon-animation-frame or addon-idle puts their own native-shaped aliases on this same facade:
| Method | Added by | Delegates to |
|---|---|---|
requestAnimationFrame(callback) | addon-animation-frame | scheduler.animation.scheduleFrame |
cancelAnimationFrame(handle) | addon-animation-frame | handle.dispose() |
requestIdleCallback(callback, options?) | addon-idle | scheduler.idle.request |
cancelIdleCallback(handle) | addon-idle | handle.dispose() |
As with the globals, a frame callback receives the frame time and an idle callback receives a deadline. Each cancel* takes the handle its request* returned rather than a numeric id, the same swap clearTimeout makes.
Compose this addon first. Those addons add to a facade this one owns, so it has to exist by the time they are applied — .use(compat).use(animation), not the other way round. Composed the wrong way round the aliases are simply absent, with no error. Their types say so: each declares its contribution as an optional compat?, so TypeScript requires the addon that owns the facade to be composed too before you can reach them without a check.
const tp = createTimeProvider.for(plugin).use(compat).use(animation).use(idle).create();
const frame = tp.compat.requestAnimationFrame(() => draw());
tp.compat.cancelAnimationFrame(frame);Naming the types
The .compat property is typed ICompatApi, its performance object ICompatPerformance, and WithCompatApi names a Time-Provider with this addon composed in:
import type { ICompatApi, WithCompatApi } from "@time-provider/addon-compat";
function pollUntilReady(compat: ICompatApi<Date>) {
compat.setInterval(() => checkReady(), 1000);
}
function schedule(tp: IUtcOnlyTimeProvider<Date> & WithCompatApi<Date>) {
pollUntilReady(tp.compat);
}CompatRuntime is also exported — the class implementing .compat on top of .scheduler.timers. Composing the addon builds one for you; construct it directly only if you need this facade outside the addon pipeline.