Skip to content

Plugins

Cross-cutting extension points that observe the session without owning browser signals themselves.

Previous: Session core · Next: Core infrastructure

Playground

Open Plugin playground → — register a plugin and inspect its hook log live.

Import path

Plugins are plain objects — no factory import needed:

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

import type { BrowserLifecyclePlugin } from "@jayoncode/browser-lifecycle";

Problem → approach

Without pluginsWith the plugin runtime
Cross-cutting logging/telemetry duplicated per featureOne BrowserLifecyclePlugin observes every public event via onEvent
No visibility into what ran, when, or how long it tookgetPluginHookLog() records every hook execution
A throwing handler can crash the whole appHook errors are isolated and reported as plugin:error
ts
const telemetryPlugin: BrowserLifecyclePlugin = {
  id: "telemetry",
  onEvent(event, payload) {
    sendBeacon(event, payload);
  },
};

const lifecycle = createBrowserLifecycle({ plugins: [telemetryPlugin] });
// or, before start():
lifecycle.use(telemetryPlugin);

Pitfalls

  • Plugins must be registered before the session starts — use() throws PluginError once the phase is "running".
  • Duplicate plugin ids throw PluginError, both via plugins: [] config and via use().
  • dependencies is metadata only today — it is exposed on plugin diagnostics but not resolved or enforced by the runtime.

Technical reference

Overview

The plugin runtime lets you observe (and react to) the full public event feed and session lifecycle without registering a browser-facing module. It is created lazily — only when plugins is configured or use() is called — so plugin-free sessions pay no cost.

The BrowserLifecyclePlugin contract

ts
interface BrowserLifecyclePlugin {
  readonly id: string;
  readonly name?: string;
  readonly version?: string;
  readonly author?: string;
  readonly description?: string;
  readonly enabled?: boolean; // default true
  readonly priority?: number; // default 0, higher runs first
  readonly dependencies?: readonly string[]; // metadata only, not enforced

  onRegister?(context: BrowserLifecyclePluginContext): void;
  onStart?(context: BrowserLifecyclePluginContext): void;
  onEvent?(event: BrowserLifecycleEventName, payload: unknown): void;
  onStop?(context: BrowserLifecyclePluginContext): void;
  onDestroy?(context: BrowserLifecyclePluginContext): void;
}

Only id is required. Every hook is optional — implement only what you need.

BrowserLifecyclePluginContext

Passed to every lifecycle hook (not onEvent, which receives the raw event name and payload instead):

ts
interface BrowserLifecyclePluginContext {
  readonly capabilities: BrowserLifecycleCapabilities;
  readonly configuration: ResolvedBrowserLifecycleConfig;
  getSnapshot(): Readonly<BrowserLifecycleSnapshot>;
}

Registering plugins

Via configuration

ts
const lifecycle = createBrowserLifecycle({
  plugins: [telemetryPlugin, analyticsPlugin],
});

Via use()

ts
const lifecycle = createBrowserLifecycle({ autoStart: false });

lifecycle.use(telemetryPlugin);
lifecycle.start();

use() throws PluginError if:

  • called after the session has started ("Plugins must be registered before BrowserLifecycle starts.")
  • called on a disposed session
  • a plugin with the same id is already registered

Plugin lifecycle

text
registered -> initialized -> started -> running -> stopped -> destroyed
HookRuns duringOrdering
onRegisterSession initialization (before first start)Higher priority first, then registration order
onStartEvery start() callHigher priority first, then registration order
onEventEvery public event, while enabled and runningHigher priority first, then registration order
onStopstop() and dispose()Reverse of start order
onDestroydispose()Reverse of start order

Plugins never receive plugin:* events through onEvent (no self-referential loops).

Runtime controls

ts
lifecycle.setPluginEnabled("telemetry", false); // skip onEvent without unregistering
lifecycle.getPlugins(); // BrowserLifecyclePluginDiagnostic[]
lifecycle.getPluginHookLog(); // BrowserLifecyclePluginHookLogEntry[] (most recent 200)

getPlugins() returns, per plugin: id, enabled, lifecycle phase, priority, hookCount, registeredAt, registrationOrder, transitions (with durationMs between phase changes), plus any of author / description / name / version / loadedAt that were provided.

getPluginHookLog() returns, per execution: id, pluginId, hook, durationMs, timestamp, and eventType (for onEvent executions).

Public Events

EventFires whenMetadata
plugin:registeredA plugin finishes onRegister{ pluginId }
plugin:removedA plugin is stopped (manual stop() or dispose()){ pluginId, reason?: "dispose" | "manual-stop" }
plugin:errorA hook throws{ hook?, pluginId }
ts
lifecycle.on("plugin:error", (event) => {
  console.error(event.metadata?.pluginId, event.metadata?.hook);
});

A throwing hook does not stop other plugins from running, and does not tear down the session — the error is caught, reported via plugin:error, and execution continues.

SSR Safety

Plugin registration and the plugin runtime itself touch no browser globals — only your plugin's own hook implementations might.

Core infrastructure →

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