1.0.2 • Published 6 months ago

@alpac/observer v1.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

Pure TypeScript Observer/Event Emitter

A flexible, type-safe, and lightweight event emitter/observer pattern implementation for TypeScript and JavaScript projects. This package provides two main classes: Observer for direct event management and Observable for easily making your classes emit events.

npm version License: MIT

Features

  • Type-Safe: Define your events and their payload types using generics. Supports object, primitive, and undefined (no payload) types.
  • Subscribe and Emit: Standard event subscription and emission.
  • Subscribe Once: Listeners that automatically unsubscribe after being called once.
  • Granular Unsubscription: Unsubscribe specific listeners, all listeners for an event, or all listeners entirely.
  • Observable Class: A convenient base class to make your own classes event-driven.
  • Clear Listeners: Easily clear once, repeating, or all listeners for an event or all events.
  • No Dependencies: Pure TypeScript implementation.
  • ESM and CJS compatible: (Assuming your build process generates both)

Table of Contents

Installation

npm install @alpac/observer
# or
yarn add @alpac/observer
# or
pnpm add @alpac/observer

Usage

1. Using Observer

The Observer class is a standalone event manager.

Defining Event Types

First, define an interface or type that maps event names to their payload types.

import { Observer, EventFunction, UnsubscribeFunction } from "@alpac/observer";

// Define your event map
interface MyCatEvents {
  meowed: { volume: number; pitch: number }; // Event with a structured object payload
  askingForFood: string; // Event with a string payload (e.g., "tuna")
  makingZoomies: undefined; // Event with no meaningful payload (payload is undefined)
  purring: undefined; // Another event with no payload
}

Subscribing to Events

const catObserver = new Observer<MyCatEvents>();

// Listener for 'meowed' (object payload)
const onMeow: EventFunction<MyCatEvents> = (payload) => {
  // payload is typed as { volume: number; pitch: number }
  console.log(`Cat meowed! Volume: ${payload.volume}, Pitch: ${payload.pitch}`);
};

catObserver.subscribe("meowed", onMeow);
// Listener for 'askingForFood' (string payload)
const onHungry: EventFunction<MyCatEvents> = (foodRequest) => {
  // foodRequest (payload) is typed as string
  console.log(`Cat is hungry and asking for: "${foodRequest}"`);
};

catObserver.subscribe("askingForFood", onHungry);
// Listener for 'makingZoomies' (undefined payload)
const onZoomies: EventFunction<MyCatEvents> = (payload) => {
  // payload is typed as undefined
  console.log("Cat is making zoomies!", payload); // payload will be undefined
};

catObserver.subscribe("makingZoomies", onZoomies);

Subscribing Once

Listeners subscribed with subscribeOnce will be automatically removed after their first invocation.

const onFirstPurr: EventFunction<MyCatEvents> = (payload) => {
  // payload is typed as undefined
  console.log("Cat started purring for the first time (in this subscription).");
  // This listener will be removed after this execution.
};

const unsubscribeFirstPurr = catObserver.subscribeOnce("purring", onFirstPurr);

Emitting Events

// Emit 'meowed' with its required object payload
catObserver.emit("meowed", { volume: 10, pitch: 5 });
// Output:
// Cat meowed! Volume: 10, Pitch: 5

// Emit 'askingForFood' with its required string payload
catObserver.emit("askingForFood", "delicious salmon");
// Output:
// Cat is hungry and asking for: "delicious salmon"

// Emit 'makingZoomies' - payload is optional and defaults to undefined
catObserver.emit("makingZoomies");
// Output:
// Cat is making zoomies! undefined

// Or explicitly pass undefined
catObserver.emit("makingZoomies", undefined);
// Output:
// Cat is making zoomies! undefined

// Emit 'purring'
catObserver.emit("purring");
// Output:
// Cat started purring for the first time (in this subscription).

// Emitting 'purring' again will not call onFirstPurr
catObserver.emit("purring");
// (No output from onFirstPurr)

Unsubscribing

You can unsubscribe using the function returned by subscribe or subscribeOnce, or by providing the event name and listener function directly.

const unsubscribeMeow = catObserver.subscribe("meowed", onMeow);

// Using the returned unsubscribe function
unsubscribeMeow();

catObserver.emit("meowed");
// (No output from onMeow)
// Using the unsubscribe method with event name and listener

catObserver.subscribe("meowed", onMeow);

catObserver.unsubscribe("meowed", onMeow);

catObserver.emit("meowed");
// (No output from onMeow)

The unsubscribe method attempts to remove the listener from both regular and once-only subscriptions. If you need more specific unsubscription:

  • observer.unsubscribeRepeating(eventName, listener)
  • observer.unsubscribeOnce(eventName, listener)

Clearing Listeners

// Clear all listeners for 'meowed' (both repeating and once)
catObserver.clear("meowed");

// Clear only repeating listeners for 'makingZoomies'
catObserver.clearRepeating("makingZoomies");

// Clear only 'once' listeners for 'purring'
catObserver.clearOnce("purring");

// Clear all listeners for all events
catObserver.clearAll();

2. Using Observable

The Observable class is designed to be extended by your classes, allowing them to emit events.

Note on Method Naming: Public methods inherited from Observable (like subscribeToEvent, emitEvent, clearEvent, etc.) are suffixed with "Event" (e.g., subscribe becomes subscribeToEvent). This consistent naming convention helps prevent naming conflicts with methods in your subclasses. For instance, the event emission method is this.emitEvent(...).

Defining Event Types

Same as with Observer. Let's use MyCatEvents again.

import { Observable, EventFunction, UnsubscribeFunction } from "@alpac/observer";

interface MyCatEvents {
  meowed: { volume: number; pitch: number };
  askingForFood: string;
  makingZoomies: undefined;
  purring: undefined;
}

Creating an Observable Class

class Cat extends Observable<MyCatEvents> {
  name: string;

  constructor(name: string) {
    super();
    this.name = name;
  }

  doMeow(volume: number, pitch: number): void {
    console.log(`${this.name} says: Meooow!`);
    // Emit an event (protected method)
    this.emitEvent("meowed", { volume, pitch });
  }

  requestFood(foodType: string): void {
    console.log(`${this.name} looks hungry and wants ${foodType}.`);
    this.emitEvent("askingForFood", foodType);
  }

  startZoomies(): void {
    console.log(`${this.name} suddenly darts across the room!`);
    this.emitEvent("makingZoomies"); // No payload needed
  }

  startPurring(): void {
    console.log(`${this.name} starts to purr contentedly.`);
    this.emitEvent("purring");
  }
}

Subscribing and Emitting

const loki = new Cat("Loki");

const onCatMeow: EventFunction<MyCatEvents> = (payload) => {
  console.log(`Monitor: Cat meowed with volume ${payload.volume}.`);
};

const unsubscribeLokiMeow = loki.subscribeToEvent("meowed", onCatMeow);

loki.subscribeToEventOnce("purring", () => {
  console.log(`Monitor: Loki started purring (this will only log once).`);
});

// Trigger events by calling methods on the Cat instance
loki.doMeow(8, 4);
// Output:
// Loki says: Meooow!
// Monitor: Cat meowed with volume 8.

loki.requestFood("tuna");
// Output:
// Loki looks hungry and wants tuna.
// (If a listener was subscribed to 'askingForFood', it would be called here)

loki.startPurring();
// Output:
// Loki starts to purr contentedly.
// Monitor: Loki started purring (this will only log once).

// Purring again won't trigger the 'once' listener
loki.startPurring();
// Output:
// Loki starts to purr contentedly.
// (No "Monitor: Loki started purring" log)

// Unsubscribe
unsubscribeLokiMeow();
// or
// loki.unsubscribeFromEvent('meowed', onCatMeow);

The unsubscribe method attempts to remove the listener from both regular and once-only subscriptions. If you need more specific unsubscription:

  • observer.unsubscribeRepeatingEvent(eventName, listener)
  • observer.unsubscribeOnceEvent(eventName, listener)

Clearing Listeners

// Clear all listeners for 'meowed' (both repeating and once)
loki.clearEvent("meowed");

// Clear only repeating listeners for 'makingZoomies'
loki.clearRepeatingEvent("makingZoomies");

// Clear only 'once' listeners for 'purring'
loki.clearOnceEvent("purring");

// Clear all listeners for all events
loki.clearAllEvents();

API Reference

Observer<T>

Manages event subscriptions and emissions. T is an object type mapping event names (keys) to payload types (values).

  • constructor()
  • subscribe<K extends keyof T>(eventName: K, event: EventFunction<T, K>): UnsubscribeFunction
  • subscribeOnce<K extends keyof T>(eventName: K, event: EventFunction<T, K>): UnsubscribeFunction
  • unsubscribe<K extends keyof T>(eventName: K, event: EventFunction<T, K>): void
  • unsubscribeRepeating<K extends keyof T>(eventName: K, event: EventFunction<T, K>): void
  • unsubscribeOnce<K extends keyof T>(eventName: K, event: EventFunction<T, K>): void
  • emit<K extends keyof T>(eventName: K, ...payloadArg: undefined extends T[K] ? [payload?: T[K]] : [payload: T[K]]): void
  • clearAll(): void
  • clear(eventName: keyof T): void
  • clearRepeating(eventName: keyof T): void
  • clearOnce(eventName: keyof T): void

Observable<T>

A base class to make other classes observable. T is an object type mapping event names to payload types. It internally uses an Observer instance.

  • constructor()
  • subscribeToEvent<K extends keyof T>(eventName: K, event: EventFunction<T, K>): UnsubscribeFunction
  • subscribeToEventOnce<K extends keyof T>(eventName: K, event: EventFunction<T, K>): UnsubscribeFunction
  • unsubscribeFromEvent<K extends keyof T>(eventName: K, event: EventFunction<T, K>): void
  • unsubscribeRepeatingEvent<K extends keyof T>(eventName: K, event: EventFunction<T, K>): void
  • unsubscribeOnceEvent<K extends keyof T>(eventName: K, event: EventFunction<T, K>): void
  • clearAllEvents(): void
  • clearEvent(eventName: keyof T): void
  • clearRepeatingEvent(eventName: keyof T): void
  • clearOnceEvent(eventName: keyof T): void
  • protected emitEvent<K extends keyof T>(eventName: K, ...payload: undefined extends T[K] ? [payload?: T[K]] : [payload: T[K]]): void

Type Aliases

  • EventFunction<T, K extends keyof T = keyof T> = (payload: T[K]) => void;
    • The function signature for an event listener. payload is typed according to the event definition in T.
  • UnsubscribeFunction = () => void;
    • A function that, when called, unsubscribes the listener it was returned for.
  • EventsMap<T> = Record<keyof T, Array<EventFunction<T>>>;
    • Internal type representing a map of event names to arrays of their listeners.
  • PartialEventsMap<T> = Partial<EventsMap<T>>;
    • A partial version of EventsMap<T>.

Contributing

Contributions are welcome! Please feel free to submit a pull request or open an issue.

  1. Fork the repository.
  2. Create your feature branch (git checkout -b feature/AmazingFeature).
  3. Commit your changes (git commit -m 'Add some AmazingFeature').
  4. Push to the branch (git push origin feature/AmazingFeature).
  5. Open a Pull Request.

License

Pure TypeScript Observer/Event Emitter is MIT licensed.

1.0.2

6 months ago

1.0.1

6 months ago

1.0.0

6 months ago