Explicit wall-clock time access for TypeScript applications.
@enormora/wall-clock provides a small boundary around time-related side effects:
- reading the current timestamp
- creating the current
Date - scheduling and clearing timeouts
- scheduling and clearing intervals
- replacing real time with a deterministic clock in tests
The package is intentionally small. It is meant to make time an explicit dependency instead of letting application code
call Date.now(), new Date(), setTimeout, or setInterval directly.
npm install @enormora/wall-clockThe package is ESM-only and requires Node.js ^24.15.0 || ^26.0.0.
The root export keeps both clocks available from one import. Prefer the explicit module subpaths when code only needs one clock:
@enormora/wall-clock/wall-clock@enormora/wall-clock/deterministic-wall-clock
Use createWallClock() at the application boundary and pass the resulting WallClock into code that needs time.
import { createWallClock } from '@enormora/wall-clock/wall-clock';
const wallClock = createWallClock();
console.log(wallClock.currentTimestampInMilliseconds);
console.log(wallClock.currentDate.toISOString());The real clock delegates to the runtime:
currentTimestampInMillisecondsusesDate.now()currentDatereturns a newDatesetTimeoutandclearTimeoutcallglobalThissetIntervalandclearIntervalcallglobalThis
The timer functions are bound to globalThis, so they can be passed around safely.
Prefer accepting a WallClock as an explicit dependency for code that depends on time.
import type { WallClock } from '@enormora/wall-clock/wall-clock';
type Session = {
readonly expiresAtTimestampInMilliseconds: number;
};
export function isSessionExpired(wallClock: WallClock, session: Session): boolean {
return wallClock.currentTimestampInMilliseconds >= session.expiresAtTimestampInMilliseconds;
}This keeps the core behavior deterministic and easy to test.
Use createDeterministicWallClock() in tests or deterministic environments.
import { createDeterministicWallClock } from '@enormora/wall-clock/deterministic-wall-clock';
const wallClock = createDeterministicWallClock({
initialCurrentTimestampInMilliseconds: 1_704_067_200_000
});
console.log(wallClock.currentDate.toISOString());
wallClock.advanceByMilliseconds(1000);
console.log(wallClock.currentTimestampInMilliseconds);The deterministic clock implements the same WallClock interface and adds:
setCurrentTimestampInMilliseconds(nextTimestampInMilliseconds)advanceByMilliseconds(delayInMilliseconds)
The deterministic clock runs scheduled callbacks when time is advanced far enough.
import assert from 'node:assert';
import { createDeterministicWallClock } from '@enormora/wall-clock/deterministic-wall-clock';
const wallClock = createDeterministicWallClock();
const calls: string[] = [];
wallClock.setTimeout(
(value) => {
calls.push(value);
},
100,
'done'
);
wallClock.advanceByMilliseconds(99);
assert.deepStrictEqual(calls, []);
wallClock.advanceByMilliseconds(1);
assert.deepStrictEqual(calls, ['done']);Intervals run once for each elapsed interval.
import assert from 'node:assert';
import { createDeterministicWallClock } from '@enormora/wall-clock/deterministic-wall-clock';
const wallClock = createDeterministicWallClock();
let count = 0;
const intervalIdentifier = wallClock.setInterval(() => {
count += 1;
}, 100);
wallClock.advanceByMilliseconds(250);
assert.strictEqual(count, 2);
wallClock.clearInterval(intervalIdentifier);
wallClock.advanceByMilliseconds(500);
assert.strictEqual(count, 2);Timeouts:
- execute once
- execute only after the clock reaches their scheduled timestamp
- execute in scheduled timestamp order
- execute in registration order when multiple timeouts share the same scheduled timestamp
- 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, and non-finite delays
The deterministic clock intentionally rejects zero-delay intervals because they cannot advance safely without creating an infinite loop.
export type WallClock = {
readonly currentTimestampInMilliseconds: number;
readonly currentDate: Date;
readonly setTimeout: <HandlerArguments extends readonly unknown[]>(
handler: (...handlerArguments: HandlerArguments) => void,
delayInMilliseconds: number,
...handlerArguments: HandlerArguments
) => ReturnType<typeof globalThis.setTimeout>;
readonly clearTimeout: (timeoutIdentifier: ReturnType<typeof globalThis.setTimeout>) => void;
readonly setInterval: <HandlerArguments extends readonly unknown[]>(
handler: (...handlerArguments: HandlerArguments) => void,
delayInMilliseconds: number,
...handlerArguments: HandlerArguments
) => ReturnType<typeof globalThis.setInterval>;
readonly clearInterval: (intervalIdentifier: ReturnType<typeof globalThis.setInterval>) => void;
};export type DeterministicWallClock = WallClock & {
readonly setCurrentTimestampInMilliseconds: (nextTimestampInMilliseconds: number) => void;
readonly advanceByMilliseconds: (delayInMilliseconds: number) => void;
};export function createWallClock(): WallClock;export function createDeterministicWallClock(options?: {
readonly initialCurrentTimestampInMilliseconds?: number;
}): DeterministicWallClock;Install dependencies:
npm clean-installCompile:
just compileRun lint checks:
just lintRun tests:
just testRun a Packtory dry-run:
just packtory-dry-runPull requests that should appear in the changelog need exactly one changelog label. Supported labels:
breakingbugfeatureenhancementdocumentationupgraderefactorbuild
The package is released through a release pull request:
- Go to GitHub Actions -> Release -> Run workflow.
- The workflow creates or updates a
Prepare releasepull request with the generatedCHANGELOG.mdchanges. - Review and merge the release pull request through the normal merge queue.
- After the release pull request is merged, the publish workflow publishes to npm, pushes the package tag, and creates the GitHub Release.
The dry-run command validates the package shape without publishing.
just packtory-dry-runInspect the next release plan:
just release-planShow what would change compared to the latest published package:
just release-diffGenerate or update the configured changelog output:
just changelogPrepare a changelog commit:
just prepare-releasePublish, tag, push tags, and create GitHub Releases:
just publish-releaseMIT