Skip to content

1. Swap the Globals ​

Moving a codebase onto Time-Provider takes three steps, and you can ship after each one:

  1. Swap the globals: prefix setTimeout, performance.now() and friends with timeProvider.compat., arguments unchanged.
  2. Use the Time-Provider API: rewrite each compat call, and each Date.now(), to the library's own API.
  3. 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\(\)" src

Create one Time-Provider ​

bash
npm install @time-provider/core @time-provider/plugin-native @time-provider/addon-compat
ts
// 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:

GlobalCompat
setTimeout, clearTimeout, setInterval, clearInterval, queueMicrotasktimeProvider.compat.<same name>
performance.now(), .timeOrigin, .mark(), .measure(), .getEntries*(), .clear*()timeProvider.compat.performance.<same name>
requestAnimationFrame, cancelAnimationFrametimeProvider.compat.<same name>, with addon-animation-frame composed too
requestIdleCallback, cancelIdleCallbacktimeProvider.compat.<same name>, with addon-idle composed too

What changes ​

  • set* and request* return an IScheduledHandle object, not a number. Pass it to the matching clear* or cancel* as before.
  • Compose the compat addon before addon-animation-frame and addon-idle, or their methods are missing from .compat.
  • Date has no compat equivalent. Each plugin returns its own date type (a Date, a Day.js object, a Luxon DateTime...), 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.