Skip to content

Session Timeout Handling

Long-lived single-page applications need to react when the user’s backend session is about to expire. The @yuuvis/client-framework library ships a turnkey solution for this: a SessionService that tracks user and HTTP activity, shows a warning before expiry, lets the user extend the session from a snackbar, and keeps every open browser tab in sync via BroadcastChannel.

The service is wired up by calling provideSession() in your application config. Once provided, it runs automatically — no component code is required for the standard flow. The service:

  • Persists the session deadline in AppCacheService (a localStorage-backed cache from @yuuvis/client-core) so every open tab reads the same expiry value
  • Subscribes to BackendService.httpCommunicationOccurred$ (a stream emitted by the @yuuvis/client-core HTTP wrapper on every backend call) and treats every HTTP call as activity
  • Listens to mousemove, keydown, click, and scroll events during a short window before expiry
  • Shows a Material snackbar one minute before expiry with an Extend session action
  • Broadcasts SessionExtended and SessionLogout messages across tabs

provideSession() is a client-level concern. In yuuvis terminology, a shell client is the top-level Angular application that bootstraps the shell, while feature apps are Angular libraries loaded into that client as extensions at runtime. The session provider belongs in the shell client’s app.config.ts and must be registered exactly once per browser context.

  • A working yuuvis shell client project (see Quick Start)
  • @yuuvis/client-framework and @yuuvis/client-core installed
  • Familiarity with Angular standalone application configuration (app.config.ts)

Add provideSession() to the providers array of your client’s ApplicationConfig:

src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideSession } from '@yuuvis/client-framework';
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
provideSession(30 * 60 * 1000) // 30 minutes
]
};

provideSession() registers SessionService and runs session.init() via provideAppInitializer at boot. init() starts the session, attaches the cross-tab listener, subscribes to HTTP activity, and installs the DOM event listeners.

There are two ways to define how long a session lasts.

If the duration is fixed and known before the app starts, pass it directly to provideSession():

src/app/app.config.ts
providers: [
provideSession(45 * 60 * 1000) // 45 minutes
]

Option 2: Dynamic duration from the backend

Section titled “Option 2: Dynamic duration from the backend”

If the duration is only known after authentication (e.g. returned in a login response), call provideSession() without arguments and update the duration later via SessionService.startSession():

src/app/app.config.ts
providers: [
provideSession() // defaults to 30 minutes until startSession() is called
]
// After login in AuthService:
login().subscribe(response => {
sessionService.startSession(response.sessionExpiresIn); // Set actual duration
});

startSession(duration) overwrites the current duration, resets the expiry timestamp to Date.now() + duration, and restarts all timers.

The service models a session as three consecutive phases before the configured expiry timestamp:

Time: T-start ───────────────► T-2min ──────────► T-1min ──────► T=0
│ │ │ │
Phase: │ Idle phase │ Activity window │ Popup │ Expiry
│ │ (2 minutes) │ (1 minute) │
│ │ │ │
│ Watches the clock │ Tracks mouse, │ Warning │ Auto-
│ (HTTP still extends │ keys, clicks, │ snackbar │ logout
│ the session) │ scroll │ with Extend │
action

Idle phase. No activity tracking. The service tracks the HTTP calls.

Activity window (default: 2 minutes before expiry). The service starts listening for DOM events and HTTP traffic. Any detected activity at the end of the window triggers an auto-extension and the cycle restarts. HTTP traffic is always tracked through BackendService.httpCommunicationOccurred$ (debounced 500 ms) — an HTTP request at any time triggers an extension, even outside the activity window.

Popup phase (default: 1 minute before expiry). If no activity was detected during the activity window, a Material snackbar appears warning the user that the session will expire. Clicking Extend session calls extendSession(); ignoring it leads to automatic logout when the timer reaches zero.

The service uses two mechanisms to keep multiple tabs consistent:

  • Shared expiry timestamp. The expiresAt value is stored under the session-expires-at key in AppCacheService (backed by localStorage). Every tab independently reads this value and schedules its own timers — there is no master/slave tab.
  • BroadcastChannel notifications. When one tab extends or logs out, it posts SessionExtended or SessionLogout on the session_channel channel. Other tabs receive the message and re-sync their timers (or perform logout) without making redundant backend calls.

Calling extendSession() issues a GET /idm/whoami request to keep the backend session alive. The yuuvis backend treats any authenticated request as session activity, so whoami doubles as a lightweight keep-alive — there is no dedicated “extend session” endpoint. This call is skipped when:

  • The extension was triggered by detected HTTP activity (the request that triggered it already counts as keep-alive)
  • The extension was triggered by a BroadcastChannel message from another tab

An internal guard prevents recursive whoami calls when the whoami response itself fires httpCommunicationOccurred$.

The session timing constants are exported from @yuuvis/client-framework and act as a read-only reference for the defaults the service uses. You cannot override these at runtime — to change the activity window or popup lead time, the constants must be changed at the framework level. The session duration, by contrast, is set per application via provideSession() or startSession().

ConstantDefaultPurpose
sessionDefaultDuration30 min (1 800 000 ms)Fallback total session length when provideSession() is called without an argument and before startSession() runs.
sessionActivityWindowBeforeEnd2 min (120 000 ms)Lead time before expiry at which the activity window opens. Must be greater than sessionPopupBeforeEnd.
sessionPopupBeforeEnd1 min (60 000 ms)Lead time before expiry at which the warning popup appears if no activity was detected.

SessionService is provided in 'root' and can be injected anywhere. For the standard flow you only need two methods.

Sets the session duration and (re)starts the lifecycle. Use this when the duration becomes known after startup — typically after a successful login.

sessionService.startSession(15 * 60 * 1000); // 15 minutes

extendSession(broadcast?, expiresAt?, skipBackendCall?): void

Section titled “extendSession(broadcast?, expiresAt?, skipBackendCall?): void”

Pushes the expiry forward and resets all timers. In normal use you do not need to call this — the service extends the session automatically based on activity, and the popup’s Extend session action calls it for you. Call it manually only when your app has a custom signal that should count as activity (e.g. a long-running operation).

ParameterTypeDefaultDescription
broadcastbooleantrueWhen true, broadcasts SessionExtended so other tabs sync.
expiresAtnumberDate.now() + durationOptional absolute expiry timestamp (ms). When omitted, the duration most recently set by provideSession() or startSession() is added to the current time.
skipBackendCallbooleanfalseWhen true, skips the whoami backend call.

The simplest setup: a static 30-minute window with all defaults.

src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideSession } from '@yuuvis/client-framework';
export const appConfig: ApplicationConfig = {
providers: [
provideSession(30 * 60 * 1000)
]
};

Defer duration setup until the backend reports it. The service uses the 30-minute default until startSession() is called.

src/app/app.config.ts
providers: [
provideSession()
]
// After login in AuthService:
login().subscribe(response => {
sessionService.startSession(response.sessionExpiresIn); // Set actual duration
});

Trigger an extension when your app finishes a long-running operation that does not produce HTTP traffic (e.g. a long client-side computation).

import { inject } from '@angular/core';
import { SessionService } from '@yuuvis/client-framework';
export class ReportComponent {
private session = inject(SessionService);
onLongReportFinished() {
this.session.extendSession();
}
}