ng2-idle-timeout
Version:
Zoneless-friendly session timeout management for Angular 16-20.
343 lines (258 loc) • 15.7 kB
Markdown
# ng2-idle-timeout
> Zoneless-friendly session timeout orchestration for Angular 16-20.
> 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](#overview--concepts)
- [Quick Start](#quick-start)
- [Configuration Guide](#configuration-guide)
- [Service & API Reference](#service--api-reference)
- [Recipes & Integration Guides](#recipes--integration-guides)
- [Additional Resources](#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.
- 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 | persistence
```
**Compatibility matrix**
| Package | Angular | Node | RxJS |
|--------------------|---------|---------|-----------|
| `ng2-idle-timeout` | 16-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.
1. **Install**
```bash
npm install ng2-idle-timeout
```
Or scaffold everything:
```bash
ng add ng2-idle-timeout
```
2. **Define shared providers**
```ts
// session-timeout.providers.ts
import { SESSION_TIMEOUT_CONFIG, SessionTimeoutService } from 'ng2-idle-timeout';
export const sessionTimeoutProviders = [
SessionTimeoutService,
{
provide: SESSION_TIMEOUT_CONFIG,
useValue: {
storageKeyPrefix: 'app-session',
idleGraceMs: 60_000,
countdownMs: 300_000,
warnBeforeMs: 60_000,
resumeBehavior: 'autoOnServerSync'
}
}
];
```
3. **Register providers with your bootstrap**
**Standalone bootstrap (`main.ts`)**
```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 { sessionTimeoutProviders } from './app/session-timeout.providers';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
...sessionTimeoutProviders
]
});
```
**NgModule bootstrap (`app.module.ts`)**
```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 import `HttpClientModule` and register `SessionActivityHttpInterceptor` in your NgModule.
4. **Start the engine once dependencies are ready**
```ts
// app.component.ts (or another shell service)
constructor(private readonly sessionTimeout: SessionTimeoutService) {}
ngOnInit(): void {
this.sessionTimeout.start();
}
```
5. **Sample usage (inject the service)**
```ts
// 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.
6. **Explore the demo**
```bash
npm run demo:start
# docs at http://localhost:4200, playground under /playground
```
Adjust the sliders, emit activity, and watch the live snapshot to confirm timers behave as expected.
---
## Configuration Guide
| Key | Default | Description |
|---------------------------|----------|-------------|
| `idleGraceMs` | `60000` | How long a session may remain idle before the countdown starts. |
| `countdownMs` | `300000` | Time window for the user to extend or acknowledge before expiry. |
| `warnBeforeMs` | `60000` | Threshold inside the countdown when WARN state triggers. |
| `activityResetCooldownMs` | `5000` | Minimum gap between automatic resets triggered by DOM/router noise. |
| `domActivityEvents` | `[ 'mousedown', 'click', 'wheel', 'scroll', 'keydown', 'keyup', 'touchstart', 'touchend', 'visibilitychange' ]` | DOM events that reset idle; add `mousemove`/`touchmove` when you explicitly want high-frequency sources. |
| `resumeBehavior` | `'manual'` | `'manual'` or `'autoOnServerSync'` for post-expiry recovery. |
| `storageKeyPrefix` | `'session'` | Namespacing prefix for persisted config and snapshots. |
| `httpActivity.strategy` | `'none'` | HTTP auto reset mode (`allowlist`, `headerFlag`, or `none`). |
| `actionDelays.start` | `0` | Debounce for throttling start/stop/pause/resume actions. |
| `logLevel` | `'warn'` | Emit verbose diagnostics when set to `'debug'`. |
**Timing cheat sheet**
```
Idle Countdown Warn Expired
|<--60s-->||<-------------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.\n\n### 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:
```ts
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. |
| `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. |
| – | `events$` | `Observable<SessionEvent>` | Structured lifecycle events (Started, Warn, Extended, etc.). |
| – | `activity$` | `Observable<ActivityEvent>` | Activity resets originating from DOM/router/HTTP/manual triggers. |
| – | `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.
### 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` (or `countdownRemainingMs$` with `async`).
- Blocking expiry route using `SessionExpiredGuard` and a focused re-authentication screen.
- Toast notifications by streaming `events$` through your notification or analytics service.
### Cross-tab and multi-device coordination
- Share a `storageKeyPrefix` across tabs so extends and expiries propagate instantly.
- Subscribe to `LeaderElected` events to gate background sync jobs to a single primary tab.
### HTTP and server alignment
- Register `SessionActivityHttpInterceptor` and configure `httpActivity` allowlists for safe auto-resets.
- Enable `resumeBehavior: 'autoOnServerSync'` when the backend can confirm the session is still valid.
- Pair `ServerTimeService` with 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 `Warn` or `Expired` occurs to understand dwell time versus active time.
- In tests, override `TimeSourceService` to 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.md` for breaking changes and upgrade hints.
- **Support & issues**: open tickets at https://github.com/ng2-idle-timeout/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!