ng2-idle-timeout
v0.3.6
Published
Zoneless-friendly session timeout management for Angular 16 and later (verified through v20).
Maintainers
Readme
ng2-idle-timeout
Zoneless-friendly session timeout orchestration for Angular 16 and later (verified through v20).
Crafted by Codex.
ng2-idle-timeout keeps every tab of your Angular application in sync while tracking user activity, coordinating leader election, and handling server-aligned countdowns without relying on Angular zones.
Contents
- Overview & Concepts
- Quick Start
- Configuration Guide
- Service & API Reference
- Recipes & Integration Guides
- Additional Resources
Overview & Concepts
What it solves
- Consolidates idle detection, countdown warnings, and expiry flows across tabs and windows.
- Survives reloads by persisting snapshots and configuration so state is restored instantly.
- Coordinates leader election across tabs and keeps shared state consistent without relying on Angular zones.
- Remains zoneless-friendly; activity sources are built on Angular signals.
How it fits together
+--------------+ activity$ +--------------------+
| Activity DOM | ----------------> | |
+--------------+ | |
| Activity | router$ | SessionTimeout | snapshot() +--------------+
| Router | ----------------> | Service | --------------> | UI / Guards |
+--------------+ | | +--------------+
| Activity HTTP| http$ | |
+--------------+ | | events$ / FX
+--------------------+
|
BroadcastChannel | storage
cross-tab | persistenceCompatibility matrix
| Package | Angular | Node | RxJS |
|--------------------|---------|---------|-----------|
| ng2-idle-timeout | 16+ (tested through 20) | >=18.13 | >=7.5 < 9 |
Quick Start
Your application might be bootstrapped with the standalone APIs (bootstrapApplication) or with an NgModule. If you do not have app.config.ts, register the sessionTimeoutProviders directly where you bootstrap (for example in main.ts or AppModule). The library works the same in both setups.
Install
npm install ng2-idle-timeoutOr scaffold everything:
ng add ng2-idle-timeoutDefine shared providers
Use
createSessionTimeoutProvidersto bundle the service and configuration once and reuse the exported config withprovideSessionTimeoutwhen bootstrapping.// session-timeout.providers.ts import { createSessionTimeoutProviders } from 'ng2-idle-timeout'; import type { SessionTimeoutPartialConfig } from 'ng2-idle-timeout'; export const defaultSessionTimeoutConfig: SessionTimeoutPartialConfig = { storageKeyPrefix: 'app-session', idleGraceMs: 60_000, countdownMs: 300_000, warnBeforeMs: 60_000, resumeBehavior: 'autoOnServerSync' }; export const sessionTimeoutProviders = createSessionTimeoutProviders(defaultSessionTimeoutConfig);Register providers with your bootstrap
Standalone bootstrap (
main.ts)import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; import { AppComponent } from './app/app.component'; import { routes } from './app/app.routes'; import { provideSessionTimeout } from 'ng2-idle-timeout'; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideSessionTimeout(() => ({ storageKeyPrefix: 'app-session', idleGraceMs: 60_000, countdownMs: 300_000, warnBeforeMs: 60_000, resumeBehavior: 'autoOnServerSync' })) ] });NgModule bootstrap (
app.module.ts)import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { sessionTimeoutProviders } from './session-timeout.providers'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule /* other modules */], providers: [...sessionTimeoutProviders], bootstrap: [AppComponent] }) export class AppModule {}If you plan to use the HTTP activity helpers, also add
provideHttpClient(withInterceptorsFromDi())in the standalone bootstrap or importHttpClientModuleand registerSessionActivityHttpInterceptorin your NgModule.Start the engine once dependencies are ready
// app.component.ts (or another shell service) constructor(private readonly sessionTimeout: SessionTimeoutService) {} ngOnInit(): void { this.sessionTimeout.start(); }Sample usage (inject the service)
// session-status.component.ts import { Component, inject } from '@angular/core'; import { DecimalPipe } from '@angular/common';
import { SessionTimeoutService } from 'ng2-idle-timeout';
@Component({ selector: 'app-session-status', standalone: true, imports: [DecimalPipe], templateUrl: './session-status.component.html' }) export class SessionStatusComponent { private readonly sessionTimeout = inject(SessionTimeoutService);
// Signals for zone-less change detection or computed view models
protected readonly state = this.sessionTimeout.stateSignal;
protected readonly idleRemainingMs = this.sessionTimeout.idleRemainingMsSignal;
protected readonly countdownRemainingMs = this.sessionTimeout.countdownRemainingMsSignal;
protected readonly totalRemainingMs = this.sessionTimeout.totalRemainingMsSignal;
protected readonly activityCooldownMs = this.sessionTimeout.activityCooldownRemainingMsSignal;
// Observable mirrors for async pipe / RxJS composition
protected readonly state$ = this.sessionTimeout.state$;
protected readonly totalRemainingMs$ = this.sessionTimeout.totalRemainingMs$;
protected readonly isWarn$ = this.sessionTimeout.isWarn$;
protected readonly isExpired$ = this.sessionTimeout.isExpired$;
protected readonly events$ = this.sessionTimeout.events$;}
```html
<!-- session-status.component.html -->
<section class="session-status">
<p>State (signal): {{ state() }}</p>
<p>State (observable): {{ (state$ | async) }}</p>
<p>Idle window: {{ (idleRemainingMs() / 1000) | number:'1.0-0' }}s</p>
<p>Countdown: {{ (countdownRemainingMs() / 1000) | number:'1.0-0' }}s</p>
<p>Total remaining (signal): {{ (totalRemainingMs() / 1000) | number:'1.0-0' }}s</p>
<p>Total remaining (observable): {{ (((totalRemainingMs$ | async) ?? 0) / 1000) | number:'1.0-0' }}s</p>
<p>Activity cooldown: {{ (activityCooldownMs() / 1000) | number:'1.0-0' }}s</p>
<p *ngIf="isWarn$ | async">Warn phase active</p>
<p *ngIf="isExpired$ | async">Session expired</p>
<ng-container *ngIf="(events$ | async) as event">
<p>Last event: {{ event.type }}</p>
</ng-container>
</section>Call sessionTimeout.start() once (as shown above) before relying on the signals; they emit immediately after bootstrap.
Every public signal on SessionTimeoutService has a matching ...$ observable that emits the same values in lockstep, so you can switch between signals and RxJS without custom bridges.
Explore the demo
npm run demo:start # docs at http://localhost:4200, playground under /playgroundAdjust the sliders, emit activity, and watch the live snapshot to confirm timers behave as expected.
Configuration Guide
| Key | Default | Description |
|-----|---------|-------------|
| idleGraceMs | 120000 | Milliseconds the session may remain idle before countdown begins. |
| countdownMs | 3600000 | Countdown window (in ms) for the user to extend or acknowledge before expiry. |
| warnBeforeMs | 300000 | Threshold inside the countdown that emits a WARN event and typically opens UI prompts. |
| activityResetCooldownMs | 0 | Minimum gap between automatic resets triggered by DOM/router activity noise. |
| domActivityEvents | ['mousedown','click','wheel','scroll','keydown','keyup','touchstart','touchend','visibilitychange'] | Events that count as user activity. Add mousemove or touchmove when you explicitly need high-frequency sources. |
| storageKeyPrefix | 'ng2-idle-timeout' | Namespace applied to persisted configuration and snapshots across tabs. |
| resumeBehavior | 'manual' | Keep manual resume (default) or enable 'autoOnServerSync' when the backend confirms session validity. |
| httpActivity.strategy | 'allowlist' | HTTP auto-reset mode ('allowlist', 'headerFlag', or 'aggressive'). |
| logging | 'warn' | Emit verbose diagnostics when set to 'debug' or 'trace'. |
| resetOnWarningActivity | true | Automatically reset the session when keyboard, mouse, scroll, or HTTP activity occurs during the countdown/warn phase. Set to false to require manual intervention once a warning is visible. |
| ignoreUserActivityWhenPaused | false | Ignore DOM/router activity while paused to prevent accidental resumes. |
| allowManualExtendWhenExpired | false | Allow operators to extend even after expiry when business rules permit it. |
When the warning phase is active the service keeps a deterministic priority order: manual > http > router > dom/cross-tab. Lower-priority activity that gets ignored exposes resetSuppressed and resetSuppressedReason metadata through activity$ so UIs and analytics can explain why a reset did not occur.
Timing cheat sheet (example)
Idle Countdown Warn Expired
|<--120s-->||<-------------300s------------->|<--60s-->|
^ idleGraceMs ^ warnBeforeMs
|<----- activity cooldown ----->|Configuration presets
- Call centre: long idle (10 min), short warn (30 s), manual resume.
- Banking: short idle (2 min), tight warn (15 s), resume only after server sync.
- Kiosk: idle disabled, countdown only, auto resume when the POS heartbeat returns.
Sync modes
'leader': default single-writer coordination. The elected tab owns persistence and rebroadcasts snapshots.'distributed': active-active coordination using Lamport clocks. Any tab may publish updates; conflicts resolve by logical clock then writer id.
Shared state metadata
Distributed snapshots embed metadata so tabs can order updates deterministically:
revisionandlogicalClocktrack monotonic progress per snapshot.writerIdidentifies the tab that produced the update.operationclarifies whether the update came from activity, pause/resume, config changes, or expiry.causalityTokende-duplicates retries and unlocks expect-reply flows.
Version 3 of the shared state schema loads older persisted payloads and upgrades them automatically, but clearing storage during deployment avoids carrying stale Lamport clocks.
DOM activity include list
domActivityEvents controls which DOM events count as user activity. The default set listens for clicks, wheel/scroll, key presses, touch start/end, and visibilitychange while leaving high-frequency sources such as mousemove and touchmove disabled to avoid noise. Add or remove events at bootstrap or at runtime:
import { DEFAULT_DOM_ACTIVITY_EVENTS } from 'ng2-idle-timeout';
sessionTimeoutService.setConfig({
domActivityEvents: [...DEFAULT_DOM_ACTIVITY_EVENTS, 'mousemove']
});Calling setConfig applies the change immediately, so you can toggle listeners when opening immersive flows (video, games) without restarting the countdown logic.
Service & API Reference
SessionTimeoutService methods
| Method | Signature | Purpose |
|--------|-----------|---------|
| start | start(): void | Initialise timers, persist a fresh snapshot, and elect a leader if needed. |
| stop | stop(): void | Reset to the initial IDLE state and clear idle/countdown timestamps. |
| pause | pause(): void | Freeze remaining time until resume() is invoked. |
| resume | resume(): void | Resume a paused countdown or idle cycle. |
| extend | extend(meta?): void | Restart the countdown window (ignores expired sessions). |
| resetIdle | resetIdle(meta?, options?): void | Record activity and restart the idle grace window. |
| expireNow | expireNow(reason?): void | Force an immediate expiry and emit Expired. |
| setConfig | setConfig(partial: SessionTimeoutPartialConfig): void | Merge and validate configuration updates at runtime. |
| getSnapshot | getSnapshot(): SessionSnapshot | Retrieve an immutable snapshot of the current state. |
| getLeaderState | getLeaderState(): SessionLeaderState | Combine the current tab role with leader metadata (ID, epoch, heartbeat). |
| getLeaderRole | getLeaderRole(): SessionLeaderRole | Quick way to read whether this tab is the leader, a follower, or awaiting election. |
| getLeaderId | getLeaderId(): string \| null | Returns the most recent leader ID known to this tab. |
| getLeaderInfo | getLeaderInfo(): LeaderInfo \| null | Returns the shared-state leader metadata when available. |
| isLeader | isLeader(): boolean | Indicates if this tab currently acts as the leader. |
| registerOnExpireCallback | registerOnExpireCallback(handler): void | Attach additional async logic when expiry happens. |
Signals and streams
| Signal | Observable | Type | Emits |
|--------|------------|------|-------|
| stateSignal | state$ | SessionState | Current lifecycle state (IDLE / COUNTDOWN / WARN / EXPIRED). |
| idleRemainingMsSignal | idleRemainingMs$ | number | Milliseconds left in the idle grace window (0 outside IDLE). |
| countdownRemainingMsSignal | countdownRemainingMs$ | number | Countdown or warn phase remaining, frozen while paused. |
| activityCooldownRemainingMsSignal | activityCooldownRemainingMs$ | number | Time until DOM/router activity may auto-reset again. |
| totalRemainingMsSignal | totalRemainingMs$ | number | Remaining time in the active phase (idle + countdown). |
| remainingMsSignal | remainingMs$ | number | Alias of total remaining time for legacy integrations. |
| isWarnSignal | isWarn$ | boolean | true when the countdown has entered the warn window. |
| isExpiredSignal | isExpired$ | boolean | true after expiry. |
| leaderRoleSignal | leaderRole$ | SessionLeaderRole | Current tab role: 'leader', 'follower', or 'unknown'. |
| leaderStateSignal | leaderState$ | SessionLeaderState | Role plus leader ID/metadata derived from shared state. |
| leaderIdSignal | leaderId$ | string \| null | Latest leader ID even while following another tab. |
| leaderInfoSignal | leaderInfo$ | LeaderInfo \| null | Leader metadata including epoch and heartbeat timestamp. |
| isLeaderSignal | isLeader$ | boolean | true while this tab owns leadership. |
| n/a | events$ | Observable<SessionEvent> | Structured lifecycle events (Started, Warn, Extended, etc.). |
| n/a | activity$ | Observable<ActivityEvent> | Activity resets originating from DOM/router/HTTP/manual triggers. |
| n/a | crossTab$ | Observable<CrossTabMessage> | Broadcast payloads when cross-tab sync is enabled. |
remainingMs$ is the same stream instance as totalRemainingMs$, preserving backwards compatibility while avoiding duplicate emissions.
Tracking leader vs follower role
The leader helpers let you drive UI that only the primary tab should see (or expose diagnostics in followers). Signals mirror observables, so you can pick whichever fits your change detection strategy:
import { Component, computed, inject } from '@angular/core';
import { SessionTimeoutService } from 'ng2-idle-timeout';
@Component({ /* ... */ })
export class SessionDiagnosticsComponent {
private readonly sessionTimeout = inject(SessionTimeoutService);
readonly leaderState = this.sessionTimeout.leaderStateSignal;
readonly role = computed(() => this.sessionTimeout.leaderRoleSignal());
readonly isLeader = computed(() => this.sessionTimeout.isLeaderSignal());
}The matching observables (leaderState$, leaderRole$, isLeader$, etc.) remain available for async pipe usage.
Tokens and supporting providers
| Token or helper | Type | Description |
|-----------------|------|-------------|
| SESSION_TIMEOUT_CONFIG | InjectionToken<SessionTimeoutConfig> | Primary configuration object (override per app or route). |
| SESSION_TIMEOUT_HOOKS | InjectionToken<SessionTimeoutHooks> | Supply onExpire or onActivity hooks without patching the service. |
| SessionActivityHttpInterceptor | Angular interceptor | Auto-reset idle based on HTTP allowlist/header strategies. |
| SessionExpiredGuard | Angular guard | Block or redirect routes when a session is expired. |
| Activity sources (DOM, router, custom) | Injectable services | Feed resetIdle() with metadata about where activity came from. |
| TimeSourceService | Injectable service | Exposes offset/offset$ so you can monitor and reset server time offsets. |
Recipes & Integration Guides
UI patterns
- Modal warning with a live countdown banner bound to
countdownRemainingMsSignal(orcountdownRemainingMs$withasync). - Blocking expiry route using
SessionExpiredGuardand a focused re-authentication screen. - Toast notifications by streaming
events$through your notification or analytics service.
Cross-tab and multi-device coordination
- Share a
storageKeyPrefixacross tabs so extends and expiries propagate instantly. - Subscribe to
LeaderElectedevents to gate background sync jobs to a single primary tab. - Use the playground diagnostics to rehearse failover and reconciliation flows before release.
HTTP and server alignment
- Register
SessionActivityHttpInterceptorand configurehttpActivityallowlists for safe auto-resets. - Enable
resumeBehavior: 'autoOnServerSync'when the backend can confirm the session is still valid. - Pair
ServerTimeServicewith jitter/backoff if backend TTL is authoritative.
Custom activity and instrumentation
- Build domain-specific activity sources (websocket heartbeats, service worker messages, analytics beacons).
- Emit analytics whenever
WarnorExpiredoccurs to understand dwell time versus active time. - In tests, override
TimeSourceServiceto deterministically advance timers and assert lifecycle events.
Additional Resources
- Docs & playground:
npm run demo:start(Angular 18 experience app at http://localhost:4200). - Release notes: see
RELEASE_NOTES.mdfor breaking changes and upgrade hints. - Support & issues: open tickets at https://github.com/ng2-idle-timeout.
Maintainer scripts
npm run build --workspace=ng2-idle-timeout- build the library with ng-packagr.npm run test --workspace=ng2-idle-timeout- run the Jest suite for services, guards, and interceptors.npm run demo:start- launch the documentation and playground app locally.npm run demo:build- production build of the experience app.npm run demo:test- sanity-check that the demo compiles in development mode.
MIT licensed - happy idling!
