Expiry
@modularityjs/expiry is a zero-dependency primitive that drives the periodic eviction pass of an in-memory TTL store. Like retry and circuit-breaker, it has no module, no DI, and no wiring — import it anywhere.
import { startExpirySweep } from '@modularityjs/expiry';
const sweep = startExpirySweep(
() => this.service.sweepExpired(),
this.config.evictionIntervalMs,
{ label: 'cache-memory' },
);
// later, in onShutdown
sweep.stop();Why it exists
Every in-memory driver of a TTL-bearing contract needs the same three things, and each one is easy to get subtly wrong on its own:
- An unref'd timer, or the sweep keeps the Node event loop alive and the process never exits.
- A
clearIntervalon shutdown, or a restarted app leaks a timer per boot. - Isolation around the sweep, or one throwing eviction pass becomes an
uncaughtExceptionand takes the process down — for a background side channel.
That combination was reimplemented six times before this primitive existed. The rule in Lean on Primitives applies to the framework's own packages too: when the same shape appears a third time, extract it.
Behaviour
| Input | Result |
|---|---|
intervalMs: undefined | Sweeping disabled; returns an inert handle whose stop() is a no-op — the documented way to opt out |
intervalMs: 0, negative, non-integer, NaN | Throws a StateException naming the store. A silently-disabled sweep is a memory leak nobody notices |
intervalMs > 2³¹−1 | Throws. setInterval would coerce it to 1 ms and sweep the whole store continuously |
sweep() throws | Warns via process.emitWarning (type: 'ModularityJsExpirySweepFailed') and keeps the schedule alive |
The returned ExpirySweep handle exposes stop() (idempotent) and a running boolean.
Who uses it
cache-memory, lock-memory, session-memory, rate-limit-memory, auth-local-password-reset-memory, and auth-local-email-verification-memory — each in its module's onInit, stopped in onShutdown. If you write a memory driver with a TTL, use this rather than a seventh copy of the same setInterval.