Skip to content

Import

TypeScript
import { Logger, Plugin, LifecycleRegistry } from '@nomercy-entertainment/nomercy-player-core';

Logger

Lightweight scoped logger. The core and every plugin share a single root instance per player.

Level ranks from least to most verbose: silent < error < warn < info < debug < trace.

TypeScript
class Logger implements ILogger {
constructor(opts?: LoggerOptions);

trace(...args: unknown[]): void;
debug(...args: unknown[]): void;
info(...args: unknown[]): void;
warn(...args: unknown[]): void;
error(...args: unknown[]): void;

level(): LogLevel;
level(value: LogLevel): void;

addSink(fn: LogSink): () => void;
child(suffix: string): Logger;
}

interface LoggerOptions {
level?: LogLevel; // default 'info'
prefix?: string;
}

type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'trace';

type LogSink = (level: LogLevel, prefix: string, args: unknown[]) => void;

addSink pipes all future log lines to an additional sink. Multiple sinks compose in registration order. Returns an unsubscribe function.

child returns a scoped sub-logger that prepends an additional prefix segment. Children share the parent’s registered sinks.

TypeScript
import { Logger } from '@nomercy-entertainment/nomercy-player-core';

const logger = new Logger({ level: 'debug', prefix: 'myapp' });
const sub = logger.child('subtitle');
sub.info('cue loaded'); // → "[myapp][subtitle] cue loaded"

// Route to a custom telemetry sink
const unsub = logger.addSink((level, prefix, args) => {
myTelemetry.log({ level, prefix, message: args.join(' ') });
});

// Remove the sink later
unsub();

Pass a custom logger via setup({ logger }) to replace the core’s default implementation. The ILogger interface is the full contract.

ILogger

The pluggable logger contract. Implement this to swap in Pino, Winston, Bunyan, or any telemetry-tapped logger.

TypeScript
interface ILogger {
trace(...args: unknown[]): void;
debug(...args: unknown[]): void;
info(...args: unknown[]): void;
warn(...args: unknown[]): void;
error(...args: unknown[]): void;

level(): LogLevel;
level(value: LogLevel): void;

addSink(fn: LogSink): () => void;
child(suffix: string): ILogger;
}

Plugin authors never construct a logger directly, they receive this.logger from the core, already scoped to their plugin id.

LifecycleRegistry

Disposable registry, every plugin gets one. Records every listener, timer, observer, abort controller, and RAF loop the plugin registers. A single dispose() call tears them all down. Idempotent: calling dispose() twice is safe.

Plugin authors never construct or dispose this directly, the player creates one per plugin and calls dispose() automatically.

TypeScript
class LifecycleRegistry {
addCleanup(fn: () => void): void;
listen(
target: EventTarget,
event: string,
handler: EventListener,
options?: AddEventListenerOptions,
): void;
timeout(fn: () => void, ms: number): number;
interval(fn: () => void, ms: number): number;
observe<O extends { disconnect(): void }>(observer: O): O;
abortable(): AbortController;
frame(fn: (deltaMs: number, time: number) => void): void;
dispose(): void;
isDisposed(): boolean;
}
MethodAuto-cleaned on dispose
listenDOM event listener removed
timeoutclearTimeout called
intervalclearInterval called
framecancelAnimationFrame called
observeObserver disconnect() called
abortableAbortController.abort() called
addCleanupCallback called in reverse registration order

observe() returns the observer unchanged so callers can chain:

TypeScript
this.lifecycle.observe(new ResizeObserver(cb)).observe(el);

frame() is silent in environments without requestAnimationFrame (Node, SSR).

Plugin

Base plugin class. Subclass to write a plugin.

TypeScript
class Plugin<
P extends IPlayer<any> = IPlayer,
O = unknown,
E extends Record<string, any> = Record<string, never>,
> {
// Required statics
static readonly id: string;
static readonly description: string;

// Recommended statics
static readonly version: string; // default '0.0.0'

// Optional statics
static readonly minCoreVersion?: string;
static readonly moduleUrl?: string; // set to import.meta.url
static readonly requires?: ReadonlyArray<RequireSpec>;
static readonly replaces?: string;
static readonly priority: number; // default 0
static readonly onError?: Record<string, PluginRecoveryAction>;
static readonly advisories?: ReadonlyArray<PluginAdvisory>;
static readonly translations?: Translations;

// Instance, set by the player before use() runs
declare player: P;
declare opts: O;
declare readonly __events__: E; // phantom type carrier

// Protected scoped helpers
protected logger: ILogger; // prefixed [nmplayer][<id>]
protected storage: IStorage; // prefixed nmplayer-<id>-

// Lifecycle
initialize(player: P, opts: O, lifecycle: LifecycleRegistry): void;
use(): void | Promise<void>;
dispose(): void;

// Standard surface
enabled(): boolean;
enable(): void;
disable(reason?: string): void;
state(): PluginState<O>;
options(): Readonly<O>;
options(partial: Partial<O>): void;

// Scoped event helpers (protected)
protected on(event, fn): void;
protected on(PluginClass, event, fn): void;
protected once(event, fn): void;
protected off(event, fn): void;
protected hasListeners(event): boolean;
protected emit(event: string, data?): void;
protected dispatchBefore<T>(event, data, opts?): Promise<BeforeDispatchResult<T>>;

// Error escalation (protected)
protected throw(payload: ThrowPayload): never;
protected report(payload: ThrowPayload): void;

// Auth-aware fetch (protected)
protected fetch<T = string>(url: string, options?: FetchOptions<T>): Promise<T>;
protected resolveUrl(url: string, category?: UrlCategory): Promise<ResolvedUrl>;

// Realtime (protected)
protected websocket(url: string, opts?: RealtimeFactoryOptions): IRealtimeChannel;

// DOM (protected)
protected mount(name: string): HTMLDivElement;
protected appendStyles(href: string, id: string): void;

// i18n (protected)
protected t(key: string, vars?: Record<string, string>): string;
protected loadTranslations?(lang: string): Promise<Record<string, string> | undefined>;

// Auto-cleaned lifecycle primitives (protected)
protected listen(target, event, handler, options?): void;
protected timeout(fn, ms): number;
protected interval(fn, ms): number;
protected frame(fn: (deltaMs: number, time: number) => void): () => void; // returns a cancel disposer
protected abortable(): AbortController;

// Class-level utilities
static derive<C>(opts, newId?): C;
clone(): typeof Plugin;
export(): O;
}

Static plugin contract

StaticRequiredDescription
idYesUnique string, 'myvendor:feature'. Lint-enforced namespacing for third-party.
descriptionYesOne-line human-readable. Used in error messages and listings.
versionRecommendedSemver string. Defaults to '0.0.0'.
minCoreVersionNoMinimum core version (semver). Checked at registration.
moduleUrlNoSet to import.meta.url. Required for appendStyles to resolve relative paths.
requiresNoDependency class refs. Required-missing throws; optional-missing warns.
replacesNoOpt-in same-id replacement. Without it, duplicate id throws core:plugin/duplicate-id.
priorityNoEvent-handler ordering. Default 0. Higher runs first.
onErrorNoPer-error-code recovery action map.
advisoriesNoDeclarative phase-aware mutation advisories.
translationsNoStatic i18n bundles. Keys must be namespaced under plugin.<id>.*.

Minimal plugin

TypeScript
import { Plugin } from '@nomercy-entertainment/nomercy-player-core';

interface MyOptions {
apiUrl: string;
}

interface MyEvents {
'data:loaded': { count: number };
}

class MyPlugin extends Plugin<IPlayer, MyOptions, MyEvents> {
static override readonly id = 'myplugin';
static override readonly description = 'Loads data from an API';
static override readonly version = '1.0.0';
static override readonly moduleUrl = import.meta.url;

override use(): void {
this.on('ready', () => {
this.logger.info('player ready');
});

this.listen(document, 'visibilitychange', () => {
this.logger.debug('visibility changed');
});
}
}

// Register the plugin before setup, then set up:
player
.addPlugin(MyPlugin, { apiUrl: 'https://api.example.com' })
.setup({});

Plugin event forms

TypeScript
// Player event, string form
this.on('play', () => {
/* handler */
});

// Another plugin's event, class form
this.on(EqualizerPlugin, 'band:changed', ({ band }) => {
console.log(band.frequency, band.gain);
});

Recovery actions

TypeScript
type PluginRecoveryAction = 'ignore' | 'disable' | 'retry-once' | 'fallback';

Map error codes to actions in static onError. When the error fires, the registry applies the action automatically:

  • 'ignore': no further action after surfacing
  • 'disable': calls this.disable(reason)
  • 'retry-once': calls this.retryLastOperation() if implemented
  • 'fallback': calls this.activateFallback() if implemented

derive and clone

Plugin.derive(opts, newId?) produces a derived class with options pre-baked. Consumer-supplied opts at registration time win (shallow merge):

TypeScript
const LoudPlugin = MyPlugin.derive({ volume: 1.0 }, 'loud-myplugin');
player.addPlugin(LoudPlugin).setup({});

clone() captures the current instance’s options via export() and calls derive().

TypeScript
const snapshot = runningPlugin.clone();

PluginState

Snapshot type returned by plugin.state(). Used by debug overlays and save/restore tooling.

TypeScript
interface PluginState<O> {
id: string;
version: string;
enabled: boolean;
opts: Readonly<O>;
runtime: Record<string, unknown>;
}

runtime is populated by overriding protected getRuntimeState() in the plugin subclass.

See also