Explicit time and timer access for TypeScript applications.
@enormora/clock provides a small dependency-injection boundary around time-related side effects:
- reading wall time as
Date, Unix epoch milliseconds, or Unix epoch microseconds - reading monotonic time and its Unix epoch origin
- scheduling and clearing timeouts
- scheduling and clearing intervals
- replacing real time with a deterministic clock in tests
It is not a date-time utility library or a general scheduler framework. The package keeps time access explicit so
application code does not need to call Date.now(), new Date(), performance.now(), setTimeout, or setInterval
directly.
npm install @enormora/clockThe package is ESM-only and requires Node.js ^24.15.0 || ^26.0.0.
The root export keeps all clocks available from one import. Prefer explicit module subpaths when code only needs one clock:
@enormora/clock/clock@enormora/clock/temporal-clock@enormora/clock/deterministic-clock
Use createClock() at the application boundary and pass the resulting Clock into code that needs time.
import { createClock } from '@enormora/clock/clock';
const clock = createClock();
console.log(clock.currentUnixEpochMilliseconds);
console.log(clock.currentUnixEpochMicroseconds);
console.log(clock.currentDate.toISOString());The real clock delegates to the runtime:
currentDatereturns a newDatecurrentUnixEpochMillisecondsusesDate.now()currentUnixEpochMicrosecondsusesDate.now() * 1000nmonotonicTimeOriginUnixEpochMicrosecondsusesperformance.timeOrigincurrentMonotonicMicrosecondsusesperformance.now()- timer functions call
globalThis
currentUnixEpochMicroseconds communicates the unit, not a guaranteed resolution. Date-backed clocks expose
millisecond-resolution wall time in microseconds.
Use createTemporalClock() when the runtime provides Temporal.
import { createTemporalClock } from '@enormora/clock/temporal-clock';
const clock = createTemporalClock();
console.log(clock.currentUnixEpochMicroseconds);The Temporal clock implements the same Clock interface. It uses Temporal.Now.instant() for wall time and
performance for monotonic time. Importing the module works without Temporal, but calling createTemporalClock()
throws when globalThis.Temporal is unavailable.
Prefer accepting a Clock as an explicit dependency for code that depends on time.
import type { Clock } from '@enormora/clock/clock';
type Session = {
readonly expiresAtUnixEpochMilliseconds: number;
};
export function isSessionExpired(clock: Clock, session: Session): boolean {
return (
clock.currentUnixEpochMilliseconds >= session.expiresAtUnixEpochMilliseconds
);
}Use wall time for calendar time, storage, and user-facing timestamps. Use monotonic values for elapsed time and durations.
Use createDeterministicClock() in tests or deterministic environments.
import { createDeterministicClock } from '@enormora/clock/deterministic-clock';
const clock = createDeterministicClock({
initialUnixEpochMicroseconds: 1_704_067_200_000_000n
});
console.log(clock.currentDate.toISOString());
clock.advanceByMilliseconds(1000);
console.log(clock.currentUnixEpochMilliseconds);The deterministic clock implements the same Clock interface and adds:
setCurrentUnixEpochMicroseconds(nextUnixEpochMicroseconds)advanceByMicroseconds(delayInMicroseconds)advanceByMilliseconds(delayInMilliseconds)
Wall time and monotonic time are stored in microseconds. Timers are scheduled against monotonic time, so setting wall time does not run or delay timers.
The deterministic clock runs scheduled callbacks when monotonic time is advanced far enough.
import assert from 'node:assert';
import { createDeterministicClock } from '@enormora/clock/deterministic-clock';
const clock = createDeterministicClock({
initialUnixEpochMicroseconds: 0n
});
const calls: string[] = [];
clock.setTimeout(
(value) => {
calls.push(value);
},
100,
'done'
);
clock.advanceByMilliseconds(99);
assert.deepStrictEqual(calls, []);
clock.advanceByMilliseconds(1);
assert.deepStrictEqual(calls, [ 'done' ]);Intervals run once for each elapsed interval.
import assert from 'node:assert';
import { createDeterministicClock } from '@enormora/clock/deterministic-clock';
const clock = createDeterministicClock({
initialUnixEpochMicroseconds: 0n
});
let count = 0;
const intervalIdentifier = clock.setInterval(() => {
count += 1;
}, 100);
clock.advanceByMilliseconds(250);
assert.strictEqual(count, 2);
clock.clearInterval(intervalIdentifier);
clock.advanceByMilliseconds(500);
assert.strictEqual(count, 2);Timeouts:
- execute once
- execute only after the clock reaches their scheduled monotonic time
- execute in scheduled monotonic time order
- execute in registration order when multiple timeouts share the same scheduled time
- can use a delay of
0 - reject negative and non-finite delays
Intervals:
- execute repeatedly
- execute once per elapsed interval when time advances
- stop after
clearInterval - reject
0, negative, non-finite, and sub-microsecond delays
The deterministic clock rejects intervals that would round down to zero microseconds because they cannot advance safely.
declare const timeoutIdentifierBrand: unique symbol;
declare const intervalIdentifierBrand: unique symbol;
export type TimeoutIdentifier = {
readonly [timeoutIdentifierBrand]: 'TimeoutIdentifier';
};
export type IntervalIdentifier = {
readonly [intervalIdentifierBrand]: 'IntervalIdentifier';
};
export type Clock = {
readonly currentDate: Date;
readonly currentUnixEpochMilliseconds: number;
readonly currentUnixEpochMicroseconds: bigint;
readonly monotonicTimeOriginUnixEpochMicroseconds: bigint;
readonly currentMonotonicMicroseconds: bigint;
readonly setTimeout: <HandlerArguments extends readonly unknown[]>(
handler: (...handlerArguments: HandlerArguments) => void,
delayInMilliseconds: number,
...handlerArguments: HandlerArguments
) => TimeoutIdentifier;
readonly clearTimeout: (timeoutIdentifier: TimeoutIdentifier) => void;
readonly setInterval: <HandlerArguments extends readonly unknown[]>(
handler: (...handlerArguments: HandlerArguments) => void,
delayInMilliseconds: number,
...handlerArguments: HandlerArguments
) => IntervalIdentifier;
readonly clearInterval: (intervalIdentifier: IntervalIdentifier) => void;
};export type DeterministicClock = Clock & {
readonly setCurrentUnixEpochMicroseconds: (
nextUnixEpochMicroseconds: bigint
) => void;
readonly advanceByMicroseconds: (delayInMicroseconds: bigint) => void;
readonly advanceByMilliseconds: (delayInMilliseconds: number) => void;
};export function createClock(): Clock;export function createTemporalClock(): Clock;export function createDeterministicClock(options: {
readonly initialUnixEpochMicroseconds: bigint;
}): DeterministicClock;