Skip to content

Events

Subscribe to typed lifecycle events with a small, framework-agnostic API.

Previous: Page lifecycle · Next: Session core

Playground

Open Event explorer → — watch the live event feed as you interact.

Import path

ts
import { createBrowserLifecycle } from "@jayoncode/browser-lifecycle";

Main package only. Prefer lifecycle.on() / returned unsub() over holding raw emitters in app code.

Problem → approach

Typical painEvent API
Custom EventTarget or ad-hoc callback arrays per featurelifecycle.on(event, handler) with typed event names
Leaked listeners after route changeson() returns unsub() — call on teardown
Untyped string events and loose payloadsSynchronous, typed handlers aligned with getSnapshot()
ts
const unsub = lifecycle.on("page:visible", () => console.log("visible"));
// later: unsub();

Pitfalls

  • Forgetting unsub() or dispose() leaks handlers across SPA routes.
  • Do not subscribe before start() / autoStart if you expect immediate browser signals.

Technical reference

This document covers the generic typed event infrastructure introduced in Phase 2.2.1.

Overview

The event infrastructure is:

  • generic
  • synchronous
  • SSR-safe
  • framework agnostic
  • free of browser-specific logic

It exists to support Browser Lifecycle modules without embedding browser semantics into the event layer itself.

Event catalog

createBrowserLifecycle() reserves the full BrowserLifecycleEventName union below. Each module page has the full metadata shape for its own events — this table is the at-a-glance index.

EventSource modulecurrent
page:visibleVisibility"visible"
page:hiddenVisibility"hidden"
window:focusFocus"focused"
window:blurFocus"unfocused"
connection:onlineConnectivity"online"
connection:offlineConnectivity"offline"
connection:reconnectConnectivity (derived by Session Core)"online"
activity:detectedIdle"active"
activity:resetIdle"active"
session:activeIdle"active"
session:idleIdle"idle"
page:suspendPage lifecycle"hidden" | "frozen" | "terminated"
page:resumePage lifecycle"active"
session:restoredPage lifecycle"running" | "stopped"
tab:primaryCross-tab"primary"
tab:secondaryCross-tab"secondary"
tab:messageCross-tab"message"
plugin:registeredPlugins"registered"
plugin:removedPlugins"removed"
plugin:errorPlugins"error"
session:startedSession core"running"
session:stoppedSession core"stopped"

Naming

Connectivity events are connection:online / connection:offline / connection:reconnectnot connectivity:*.

Every event payload shares the same envelope (BrowserLifecycleEvent<TType, TCurrent, TPrevious, TMetadata>):

ts
interface BrowserLifecycleEvent<TType, TCurrent, TPrevious, TMetadata> {
  readonly type: TType;
  readonly current: TCurrent;
  readonly previous: TPrevious;
  readonly metadata: TMetadata;
  readonly snapshot: BrowserLifecycleSnapshot; // full snapshot at emission time
  readonly source:
    | "activity"
    | "connectivity"
    | "focus"
    | "internal"
    | "lifecycle"
    | "plugin"
    | "transport"
    | "visibility";
  readonly timestamp: number;
}

Public API

TypedEventEmitter<TEventMap>

The public emitter surface is intentionally small:

  • on(event, listener)
  • off(event, listener)
  • once(event, listener)
  • emit(event, payload, options?)
  • listeners(event)
  • listenerCount(event?)
  • hasListeners(event?)
  • removeAll(event?)
  • destroy()

Constructor Options

definitions

Optional event definitions stored by the internal registry for diagnostics and statistics.

onListenerError

Optional error-isolation callback invoked when a listener throws during synchronous dispatch.

timeProvider

Optional timestamp provider used for deterministic tests and custom time sources.

Metadata

Each dispatch produces metadata with:

  • type
  • timestamp
  • source
  • dispatchId
  • listenerCount
  • internal

The metadata is created by the dispatcher and passed to every listener for the current emission.

Internal Architecture

The implementation is split into:

  • listener-collection.ts
  • event-registry.ts
  • subscription-manager.ts
  • event-dispatcher.ts
  • typed-event-emitter.ts

Event flow is:

text
module or caller
  -> TypedEventEmitter.emit()
  -> EventDispatcher
  -> EventRegistry
  -> ListenerCollection
  -> developer callback

Error Isolation

Listener execution is synchronous and ordered, but one failing listener does not prevent later listeners from running. The registry records the isolated error, and the optional onListenerError hook receives the error context.

Examples

Basic Subscription

ts
import { TypedEventEmitter } from "@jayoncode/browser-lifecycle";

interface AppEvents {
  "app:ready": { readonly id: string };
}

const emitter = new TypedEventEmitter<AppEvents>();

emitter.on("app:ready", (payload) => {
  console.log(payload.id);
});

emitter.emit("app:ready", { id: "session-1" });

One-Time Listener

ts
import { TypedEventEmitter } from "@jayoncode/browser-lifecycle";

interface AppEvents {
  "app:ready": { readonly id: string };
}

const emitter = new TypedEventEmitter<AppEvents>();

emitter.once("app:ready", (payload) => {
  console.log(payload.id);
});

Removing Listeners

ts
import { TypedEventEmitter } from "@jayoncode/browser-lifecycle";

interface AppEvents {
  "app:ready": { readonly id: string };
}

const emitter = new TypedEventEmitter<AppEvents>();
const listener = (payload: AppEvents["app:ready"]) => {
  console.log(payload.id);
};

emitter.on("app:ready", listener);
emitter.off("app:ready", listener);

Destroying an Emitter

ts
import { TypedEventEmitter } from "@jayoncode/browser-lifecycle";

interface AppEvents {
  "app:ready": { readonly id: string };
}

const emitter = new TypedEventEmitter<AppEvents>();

emitter.destroy();

An ecosystem of independent, headless TypeScript libraries engineered for modern web development. Every package includes interactive playgrounds and documentation that evolves alongside the code.