npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@financedistrict/apps-idle-session

v0.4.1

Published

Cross-tab idle-timeout detection for FD apps — tracks aggregate inactivity across every open tab on the same origin and fires a callback once none of them have seen activity for the configured timeout.

Readme

@financedistrict/apps-idle-session

Cross-tab idle-timeout detection for FD apps. Every FD app that mounts this hook on the same origin (e.g. everything under apps.test.1stdigital.tech) shares one idle clock: activity in any open tab keeps the session alive; only once every tab has been quiet for the configured timeout does onIdle fire.

Why cross-tab, not per-tab

A per-tab timer logs a user out while they're actively working in a different tab on the same origin — this package exists specifically to avoid that. It broadcasts activity through a shared localStorage key (not the usual app-namespaced kind — the whole point is that every app reads and writes the same one) and reconciles across tabs via the storage event, plus a visibilitychange recheck to catch up when a backgrounded tab's own timer gets throttled by the browser.

Usage

import { useIdleSession } from "@financedistrict/apps-idle-session";
import { useMsal } from "@azure/msal-react";

function App() {
  const { instance } = useMsal();

  const { isWarning, secondsRemaining } = useIdleSession({
    timeoutMs: 16 * 60 * 1000, // 16 minutes total
    warningMs: 60 * 1000, // last 60s of that shows a warning
    onIdle: () => instance.logoutRedirect(),
  });

  return (
    <>
      <Routes />
      {isWarning && <IdleWarningDialog secondsRemaining={secondsRemaining} />}
    </>
  );
}

Mount the hook once, near the root, alongside the other session-scoped providers. onIdle is expected to end the session — an MSAL logout is the norm — the hook itself never touches auth state or calls MSAL. Rendering a countdown UI for isWarning is the caller's job too (apps-ui's IdleWarningDialog preset, built for this).

API

const { isWarning, secondsRemaining } = useIdleSession({
  timeoutMs: number; // required — ms of aggregate cross-tab inactivity before onIdle fires
  onIdle: () => void; // required — called once, in every open tab, when the timeout is reached
  warningMs?: number; // default: none — report isWarning starting this many ms before timeoutMs
  activityEvents?: string[]; // default: mousedown, mousemove, keydown, scroll, touchstart, wheel
  activityThrottleMs?: number; // default: 1000 — how often activity is broadcast to other tabs
  checkIntervalMs?: number; // default: 5000 — how often each tab re-derives elapsed idle time
  storagePrefix?: string; // default: "fd:idle-session" — shared across every app on the origin;
  // override only if this app deliberately wants its own, isolated idle clock
  enabled?: boolean; // default: true
});

Returns { isWarning: boolean; secondsRemaining: number | null }. Without warningMs, isWarning stays false and secondsRemaining stays null for the hook's whole lifetime — the pre-warning behavior, unchanged. With warningMs set, isWarning flips to true once timeoutMs - warningMs has elapsed since the last activity, and secondsRemaining ticks down once a second from there (a separate, faster internal poll than checkIntervalMs — otherwise a countdown driven by the default 5s check would visibly jump in 5-second steps). Any activity — local or broadcast from another tab — clears isWarning immediately, without waiting for the next tick.

onIdle fires once per tab, per idle period. A tab that already fired won't fire again until fresh activity resumes the shared clock — in practice a page reload after logout resets this naturally.

If you pass a custom activityEvents array, memoize it (useMemo): it's a dependency of the hook's internal effect, so a fresh array identity on every render resubscribes the listeners every render.

What this package does not do

  • It does not call MSAL, or decide what "logged out" means for your app — that's onIdle's job.
  • It does not render anything. isWarning/secondsRemaining are state, not UI — pair with apps-ui's IdleWarningDialog (or your own) to actually show a countdown.
  • Like a route guard, this is a UX affordance, not a security control: actual session and token lifetime are enforced by MSAL and the BFF, not by this timer. A tampered or bypassed client-side timer never grants extra access.

useStaySignedIn — the 24h auto-logout grant

A separate, related hook for a device-level "disable auto-logout" grant. There is no login-time prompt (retired in ADR-0098): Entra already keeps the user signed in, so there was nothing to confirm right after login — the only real decision point is the idle-warning countdown itself, so the grant is offered from IdleWarningDialog's checkbox, not a separate dialog.

import {
  useIdleSession,
  useStaySignedIn,
} from "@financedistrict/apps-idle-session";
import { IdleWarningDialog } from "@financedistrict/apps-ui/idle-warning-dialog";

function App() {
  const { isAuthenticated, instance } = useAuth();

  const { isSuppressingIdleLogout, acceptStaySignedIn } = useStaySignedIn({
    enabled: isAuthenticated,
  });

  const { isWarning, secondsRemaining } = useIdleSession({
    timeoutMs: 31 * 60 * 1000,
    warningMs: 60 * 1000,
    onIdle: () => instance.logoutRedirect(),
    enabled: isAuthenticated && !isSuppressingIdleLogout,
  });

  return (
    <>
      <Routes />
      <IdleWarningDialog
        open={isWarning}
        secondsRemaining={secondsRemaining ?? 0}
        onSignOut={() => instance.logoutRedirect()}
        onContinue={(disableAutoLogoutFor24h) => {
          if (disableAutoLogoutFor24h) acceptStaySignedIn();
        }}
      />
    </>
  );
}
const { isSuppressingIdleLogout, acceptStaySignedIn } = useStaySignedIn({
  enabled?: boolean; // default true — gate this on isAuthenticated
  staySignedInMs?: number; // default DEFAULT_STAY_SIGNED_IN_MS (24h) — leave at the
  // shared default so an accepted grant means the same thing in every app
  storagePrefix?: string; // default: same "fd:idle-session" family as useIdleSession —
  // "stay signed in" is a device-level intent, meant to apply to every FD app
});
  • isSuppressingIdleLogout is what actually gates useIdleSession's enabled. Re-checked periodically (every 60s), so a grant correctly expires on its own after staySignedInMs even if the tab is never reloaded in between.
  • acceptStaySignedIn() starts a new staySignedInMs grant, immediately, in every tab on this origin. Call it when IdleWarningDialog's onContinue fires with its checkbox checked — there is no separate "don't show again" preference to thread through; the grant itself is the only state that persists.
  • One thing this can't do: "stay signed in" only ever means "don't run our own idle-logout for a while." It has no control over Entra's actual session/token lifetime — if that expires on its own first (tenant sign-in-frequency policy), the user is signed out regardless of any active grant here.