1. Swap the Globals
Moving a codebase onto Time-Provider takes three steps, and you can ship after each one:
- Swap the globals: prefix
setTimeout,performance.now()and friends withtimeProvider.compat., arguments unchanged. - Use the Time-Provider API: rewrite each compat call, and each
Date.now(), to the library's own API. - Inject the Time-Provider: have your code receive the Time-Provider, so a test can hand it a clock it controls.
Find the call sites
bash
grep -rnE "\b(set|clear)(Timeout|Interval)\(|queueMicrotask\(|(request|cancel)(AnimationFrame|IdleCallback)\(|performance\.|Date\.now\(|new Date\(\)" srcCreate one Time-Provider
bash
npm install @time-provider/core @time-provider/plugin-native @time-provider/addon-compatts
// time-provider.ts
import { createTimeProvider } from "@time-provider/core";
import { plugin } from "@time-provider/plugin-native";
import { addon } from "@time-provider/addon-compat";
export const timeProvider = createTimeProvider.for(plugin).use(addon).create();Pick the plugin matching the date library you already use.
Prefix the calls
Before
ts
const poll = setInterval(refresh, 30_000);
const start = performance.now();
setTimeout(() => {
clearInterval(poll);
report(performance.now() - start);
}, 60_000);After
ts
import { timeProvider } from "./time-provider";
const poll = timeProvider.compat.setInterval(refresh, 30_000);
const start = timeProvider.compat.performance.now();
timeProvider.compat.setTimeout(() => {
timeProvider.compat.clearInterval(poll);
report(timeProvider.compat.performance.now() - start);
}, 60_000);Everything the compat addon covers:
| Global | Compat |
|---|---|
setTimeout, clearTimeout, setInterval, clearInterval, queueMicrotask | timeProvider.compat.<same name> |
performance.now(), .timeOrigin, .mark(), .measure(), .getEntries*(), .clear*() | timeProvider.compat.performance.<same name> |
requestAnimationFrame, cancelAnimationFrame | timeProvider.compat.<same name>, with addon-animation-frame composed too |
requestIdleCallback, cancelIdleCallback | timeProvider.compat.<same name>, with addon-idle composed too |
What changes
set*andrequest*return anIScheduledHandleobject, not a number. Pass it to the matchingclear*orcancel*as before.- Compose the compat addon before
addon-animation-frameandaddon-idle, or their methods are missing from.compat. Datehas no compat equivalent. Each plugin returns its own date type (aDate, a Day.js object, a LuxonDateTime...), so dates move straight to the clock in step 2.
Keep migrated files clean
An ESLint rule on the folders you have migrated stops new native calls from slipping back in:
js
// eslint.config.js
export default [
{
files: ["src/billing/**"],
rules: {
"no-restricted-globals": [
"error",
"setTimeout",
"clearTimeout",
"setInterval",
"clearInterval",
"queueMicrotask",
"performance",
"requestAnimationFrame",
"cancelAnimationFrame",
"requestIdleCallback",
"cancelIdleCallback",
],
},
},
];Widen files as the migration moves along.