An awaitable stopwatch/timer with cancellation, handler execution, reset/refresh, and remaining-time inspection.
Version 4 separates execution completion from cancellable waiting and adopts released contexts v3.0.0 and async-cancellable v3.0.0. Consumers of version 3's lazy/eager and injected-controller APIs must migrate deliberately; changing their dependency alone is not sufficient. Existing tags retain their original API.
npm install --save 'git+ssh://git@github.com/MatrixAI/js-timer.git#v4.0.0'import { Timer } from '@matrixai/timer';
const timer = new Timer({
delay: 100,
handler: (signal) => (signal.aborted ? 'cancelled' : 'done'),
});
const remaining = timer.getTimeout();
timer.refresh();
const result = await timer;The two-argument positional constructor is also supported. Cancelling pending scheduling rejects with the exact reason and prevents handler entry. Cancelling a running handler requests cooperation; awaiting the timer still observes the handler's actual result. Infinite delays remain pending until cancelled or reset to a finite delay. Reset and refresh reject changes after execution starts.
Use timer.wait(signal) to abandon only one caller's wait. It never cancels the
timer. The always-fulfilled timer.completion records the immutable actual
execution outcome; it cannot be cancelled by a waiter. A noncooperative handler
can keep completion pending indefinitely. Native chains do not forward
cancellation automatically.
The previous lazy/eager flags and injected-controller overloads are removed. Use explicit execution cancellation or waiter cancellation instead. Attaching a signal listener does not silently select policy. No performance improvement is claimed by this redesign.
Timer uses the canonical contexts 3.0.0 scope contract. Applications compose the provider-owned scope rather than implement timer ownership repeatedly:
import type { ContextOperation } from '@matrixai/contexts';
import type { TimerContext } from '@matrixai/timer';
import { createTimerScope } from '@matrixai/timer';
const provideTimer = createTimerScope({ delay: 10, handler: () => 7 });
const work: ContextOperation<TimerContext<number> & { trace: string }, string> =
async ({ timer, trace }) => `${trace}:${await timer}`;
const run: ContextOperation<{ trace: string }, string> = provideTimer(work);
const result = await run({ trace: 'application-owned' });Construction is inert. Each execution owns a fresh timer; overlapping calls do not share timer state. Other context requirements remain the caller's job. This scope can nest directly with other contexts scopes in application-selected order. The application still owns retries, supervision, and shutdown policy.
On scope completion, pending scheduling is cancelled and any started handler is awaited before outer resources may be released. Cancellation requests do not force a running handler to complete. A handler that never finishes therefore prevents scope completion; callers must design cooperative termination, not release live resources on a timeout race.
Successful cleanup preserves the body's exact result or rejection. A handler
failure not already propagated by the body is a release failure. If both fail,
contexts preserves the handler failure as SuppressedError.error and the body
failure as SuppressedError.suppressed. The identical propagated handler failure
is not reported twice. Pending scheduling cancelled by scope release is normal
cleanup, not an application failure.
The scope does not acquire borrowed timers, supply a cancellation controller, link a parent signal, or interpret a refreshable timer as a fixed deadline. Its handler receives the timer-owned cancellation signal. Generic cancellation sources, linking, and borrowed waits come from async-cancellable rather than a second timer-specific cancellation implementation.
constrainDeadline and createDeadlineConstraintScope intersect finite absolute
constraints from the same clock identity without scheduling or cancelling work.
They use the smaller timestamp and reject incomparable clocks.
createDeadlineScope enforces a captured absolute bound with an owned signal and
scheduler; createTimeoutScope computes a fresh relative budget on every entry.
Neither widens an inherited deadline. Wrapping a retry loop gives a total budget;
putting the timeout inside that loop gives each attempt a fresh budget, still
bounded by any outer constraint. Expiry requests cancellation, not early scope
completion. Owned scheduling is stopped and monitored work drained on release.
The minimum laws apply to fixed same-clock constraints, not mutable reset/refresh
timers or arbitrary handler executions.
Timer source uses the global Performance API when usable, with a synchronous wall-clock fallback for missing, incomplete, restricted, non-finite, or out-of-range clocks. Construction, remaining-time reads, and reset/refresh all use the same internal clock. There are no Node builtin imports in timer source, no asynchronous clock initialization, and no separate runtime-specific builds.
Timestamps and deadlines are epoch milliseconds, not raw relative performance readings. Clock readings are weakly nondecreasing within one loaded clock module instance. Millisecond precision permits equality. A backwards wall-clock change can pause the estimated countdown until the clock catches up. This is not a cross-worker/process clock, a unique ID, or a promise of accurate elapsed time when falling back to a wall clock. A shorter reset delay may move a deadline earlier; the creation timestamp does not change.
Scheduling still uses the host's native timeouts/intervals. Browser numeric handles and Node timer objects do not require separate implementations. Host scheduling delays, throttling, suspension, and clock precision remain outside the timer's control. Pending timers keep the Node event loop alive like native timers. Remaining time becomes zero once settling begins, even if a handler has not completed.
The errors dependency remains selected by its released Git tag over SSH. The
contexts dependency selects the private Git-only v3.0.0 release; the npm lock
pins its exact commit dc958bdffdd535cd347d9fe6f800d91177ede341. Cancellation
selects its Git-only v3.0.0 release at
8566e758e19d18fbd729af28f21a770768d5dcdc. Authorized SSH access is required
for acquisition. Older npm-registry releases do not supply these APIs.
These selections use the existing async-init consumer pattern: the npm lock owns the exact revision, the Nix source loader selects its prepared archive and validates the producer descriptors, and native/sandbox installation uses that archive without rewriting the packed manifest. There is no direct contexts flake input, second pin, local source override, or extra package lock. Credentials are not embedded in URLs or archives, and private artifacts must not be uploaded to a public binary cache without separate authority.
For explicit local provider experiments, npm run check:candidate -- /absolute/cancellation.tgz --browser creates an isolated consumer with a
generated dependency edge. Add --lifecycle-root /absolute/async-init-checkout
after that checkout's packed-package check to exercise admission, race losers,
timer cleanup, and service stop ordering. This optional lane preserves the
lifecycle package's own locked dependency scopes. It is not the canonical
dependency-selection or release gate, and its generated archive is not a release.
Use Node.js 24 or newer. The flake provides integrated
flake-parts/devenv and direnv activation. Review the configuration and
run direnv allow .. The prepared-dependency npm adapter serializes
installation and restores canonical manifests after install-only source
overrides. Activation does not load credentials or change persistent shell
options. Once activated, run project commands directly.
git submodule update --init --depth 1
nix run './.matrixai/matrixai-standards#skills-materializer' -- --standards-root ./.matrixai/matrixai-standards
npm run lintfix
npm run build
npm run typecheck
npm test -- --runInBand
npm run check:package
npm run check:portable
npm run checkTests compile first and use Jest/SWC in ESM mode against emitted modules.
Arguments pass through npm test. JUnit is written under tmp/junit; optional
V8/Cobertura coverage is produced with npm test -- --runInBand --coverage. Use
npm run lintfix for active development and npm run lint for non-mutating
verification. Documentation is handwritten at the repository root; there is no
TypeDoc dependency, generated documentation directory, or installer test suite.
MatrixAI lint comes from the selected public nixpkgs-matrixai package set, not
an npm dependency or a direct js-lint flake input. One configured tool
environment is shared by the development shell, flake formatter, and independent
hermetic lint check. The bundled CLI owns configuration; TypeScript remains a
consumer development dependency. The packaged-consumer type fixture is compiled
independently by npm run check:package, not against the main lint TypeScript
project.
nix fmt
npm run lintfix
npm run lint
nix build '.#checks.x86_64-linux.lint'The first two commands share the formatting authority across applicable domains, including TypeScript, Markdown, shell, and Nix. The last two are non-mutating verification; the Nix lint gate works without an activated development shell.
nix build '.#npm-package'
nix build '.#npm-package-unpacked'
nix build '.#checks.x86_64-linux.default'
nix build '.#checks.x86_64-linux.runtime'The default package and npm-package are the archive file itself. The unpacked
output contains the exact package contents at its root. Compiled ESM,
declarations, source maps, canonical metadata, README, and license are included;
dependencies, Node, and Nix paths are not bundled into the package metadata.
Source and packed manifests preserve dependency declarations and lifecycle
scripts verbatim.
Schema-v1 package/distribution descriptors expose manifest metadata and honest source provenance. Cancellation, contexts, and errors are advertised as selected direct runtime providers. Contexts has an empty runtime dependency set; its test dependencies do not become timer dependencies. Timer remains both a consumer of prepared archives and a producer of its own archive, with no bundled dependency tree.
The npm lock, not distribution metadata, owns the transitive dependency graph. Select each required Git-backed dependency at its concrete locked module path; nested copies may select different revisions. The shared installer and sandbox adapter resolve edges from the nearest package scope, normalize only temporary installation constraints, and restore canonical manifests. Unprepared Git dependencies fail closed in the sandbox. Runtime-provider advertisements do not silently acquire packages, deduplicate versions, or change a consumer's lock. Lint is supplied separately by the package set. Registry dependencies retain their integrity-pinned tarballs. Hermetic installs use importNpmLock with its matching Node installation hook.
The check layout follows the container repository:
routine verify and independent lint gates compose default, separate from
integration-browser and its runtime aggregate. Checks call the same npm
scripts without depending on the active development shell. Successful outputs
retain report.json and artifacts.
npm run check:package inspects a packed consumer reconstructed from the
lockfile-installed runtime dependency tree; it does not test npm installation.
npm run check:portable rejects Node builtins in timer source and reports
transitive builtins separately. It is not a browser execution claim.
npm run test:browser attempts a real packed-package Chromium document and
dedicated-worker run without aliases.
See verification evidence for executed results. Consumer updates require a new authorized release/tag. Git-only releases do not publish to npm; the obsolete tag-triggered npm publisher is removed.