Addons (Extensions) — Overview
A plugin only ever bridges a date library into the ITimeProvider pipeline — it adds no new functionality. An addon extends the Time-Provider a builder produces with an extra property, composed in with .use(addon) before .create() (or before picking a deterministic strategy):
import { createTimeProvider } from "@time-provider/core";
import { plugin } from "@time-provider/plugin-native";
import { addon } from "@time-provider/addon-animation-frame";
const timeProvider = createTimeProvider.for(plugin).use(addon).create();
timeProvider.scheduler.animation.scheduleFrame(() => console.log("Frame!"));timeProvider above is still a plain ITimeProvider<Date> — clock, converter, scheduler, performance — plus whatever the addon adds, here a scheduler.animation facade exposing scheduleFrame. An addon that schedules callbacks extends scheduler; one that doesn't adds a root property of its own.
| Addon | Property | Adds | Contributed type | npm |
|---|---|---|---|---|
addon-animation-frame | scheduler.animation | scheduleFrame — the host's real frames, or simulated ones | WithAnimationFrameApi | npm |
addon-cron | scheduler.cron | callbacks on 5-field cron schedules, read in the runtime's own timezone | WithCronApi | npm |
addon-eta | .eta | estimates of when a job finishes, from reported progress or a fixed expected duration | WithEtaApi | npm |
addon-idle | scheduler.idle | request - callbacks run when the host reports itself idle, or on demand in a test | WithIdleApi | npm |
addon-compat | .compat | native-style setTimeout/setInterval/queueMicrotask and performance signatures, for migrating incrementally | WithCompatApi | npm |
Each one peer-depends on @time-provider/core and nothing else, so composing an addon adds no third-party package to your dependency tree.
Naming the composed type
Inference picks all of that up, so you don't normally write the type down. If you need to — a parameter in a shared helper, say — each addon exports the shape it contributes, to intersect with the provider type:
import type { WithAnimationFrameApi } from "@time-provider/addon-animation-frame";
function animate(tp: ITimeProvider<Date> & WithAnimationFrameApi) {
tp.scheduler.animation.scheduleFrame(() => {});
}@time-provider/addon-cron exports WithCronApi and @time-provider/addon-eta exports WithEtaApi the same way. Annotating the build site instead would drop the addon's property, so prefer inference there — see Naming these types.
Addons are split by entry point too
Just like plugins, an addon that needs to behave differently on a deterministic clock ships two entry points. Compose the matching addon with the matching createTimeProvider/plugin:
import { createTimeProvider } from "@time-provider/core/deterministic";
import { plugin } from "@time-provider/plugin-native/deterministic";
import { addon } from "@time-provider/addon-animation-frame/deterministic";
const timeProvider = createTimeProvider
.for(plugin)
.use(addon)
.asManual()
.withInitialTime(0)
.create();
timeProvider.scheduler.animation.scheduleFrame(() => console.log("Frame!"));
timeProvider.clock.advance({ milliseconds: 20 }); // simulated frame duration elapses@time-provider/addon-cron and @time-provider/addon-eta behave the same on both sides — they read time through clock.timestampNow() and program timers on timeProvider.scheduler, so the clock strategy already decides when their callbacks run. Two addons differ. @time-provider/addon-animation-frame calls the host's requestAnimationFrame on the system side and simulates frames against the runtime's own clock on the deterministic one. @time-provider/addon-idle calls the host's requestIdleCallback on the system side, while its deterministic half holds every request pending until a test calls idle.drain(), since a simulated clock has no idle to detect.
Composing more than one
.use(...) chains, and each addon contributes its own property:
import { addon as cron } from "@time-provider/addon-cron";
import { addon as eta } from "@time-provider/addon-eta";
const timeProvider = createTimeProvider.for(plugin).use(cron).use(eta).create();
timeProvider.scheduler.cron.schedule("0 9 * * *", () => reindex());
timeProvider.eta.estimate();Each addon's addon export is a factory function, not a shared instance: .use(...) calls it to get a fresh addon-builder, so composing the same import with two Time-Providers never shares state between them.
Want a facade of your own? See Writing a Custom Addon.