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

semantically

v1.0.9

Published

CQRS/Event Sourcing library for coding semantically

Readme

semantically ⚡️

A minimal CQRS / Event Sourcing helper library for TypeScript that favors clear, semantic domain code.

  • Package: semantically
  • Purpose: Provide base classes and decorators for defining Aggregate roots, strongly-typed domain Events, Event Handlers, and a simple AggregateRepository pattern.

Quick summary ✅

  • Define events that extend AggregateEvent and annotate them with @Event(...).
  • Define handlers that extend EventHandler<T> and annotate with @Handles(...).
  • Implement aggregates by extending Aggregate<TId> and use applyEvent(...) to register uncommitted domain events.
  • Use an AggregateRepository implementation which takes a list of handlers and calls them on save, then commits events on the aggregate.

Installation

npm install semantically
# dev scripts
npm test
npm run build

Usage examples 🔧

Defining events

import { AggregateEvent, Event } from 'semantically';

@Event(UserCreatedEvent)
class UserCreatedEvent extends AggregateEvent {
  constructor(public readonly id: string, public readonly email: string) {
    super(1); // event version
  }
}

Events get:

  • eventId (uuid),
  • recordedAt (Date),
  • version (number)

The @Event decorator sets an internal __name property used to match handlers to events.

Aggregate roots

import { Aggregate } from 'semantically';

class UserAggregate extends Aggregate<string> {
  private email!: string;

  constructor(id: string, email: string) {
    super(id);
    this.registerEventHandlers();
    this.applyEvent(new UserCreatedEvent(id, email));
  }

  override registerEventHandlers(): void {
    this.on(UserCreatedEvent, (evt) => {
      this.email = evt.email;
    });
  }

  updateEmail(newEmail: string) {
    if (this.email !== newEmail) {
      this.applyEvent(new UserEmailUpdatedEvent(this.id, newEmail, this.version));
    }
  }
}

Notes:

  • registerEventHandlers() is where you wire up all event listeners using this.on(...). Call it at the top of your constructor.
  • applyEvent(event) adds an uncommitted domain event to the aggregate.
  • The aggregate exposes getEvents() to read uncommitted events.
  • Call commit(event) (or let a repository call it) to emit the event and remove it from the uncommitted list.

Snapshot rehydration

When loading an aggregate from a read store rather than replaying events, use the snapshot overload of on. Pass only a listener with a type parameter — the library automatically routes it to the 'snapshot' event name:

type UserSnapshot = {
  id: string;
  email: string;
  version: number;
};

override registerEventHandlers(): void {
  this.on(UserCreatedEvent, (evt) => {
    this.email = evt.email;
  });

  this.on<UserSnapshot>((snapshot) => {
    this.email = snapshot.email;
    this.version = snapshot.version;
  });
}

The type parameter shapes the listener argument — no extra base class or decorator needed. This overload is visually distinct from regular event handlers, making it clear that snapshot loading is a special case.

Event handlers and decorators

import { Handles, EventHandler } from 'semantically';

@Handles(UserCreatedEvent)
class SendWelcomeEmail extends EventHandler<UserCreatedEvent> {
  async handle(event: UserCreatedEvent): Promise<void> {
    // send email or persist projection etc.
  }
}

The @Handles decorator sets an internal __handles property so the repository can route events to matching handlers.

AggregateRepository

Implement a repository by extending AggregateRepository<TId, T> and providing a get(id) method.

class UserRepository extends AggregateRepository<string, UserAggregate> {
  async get(id: string): Promise<UserAggregate | null> {
    // load persisted state and return rehydrated aggregate or null
  }
}

// instantiate with handlers
const repo = new UserRepository([new SendWelcomeEmail()]);

// when saving an aggregate the repo runs handlers and commits events
await repo.save(userAggregate);

API Reference

Exports (from index.ts):

  • Aggregate<TId> - base class for aggregates
  • AggregateEvent - base class for domain events
  • EventHandler<TEvent> - base class for handlers
  • AggregateRepository<TId, T extends Aggregate<TId>> - repository base class
  • @Event(...) decorator - annotate event classes
  • @Handles(...) decorator - annotate handler classes

Tests & Development 🧪

  • Run tests: npm test (uses jest)
  • Build: npm run build (emits dist/)

The repo contains comprehensive unit tests that demonstrate intended usage patterns (see src/*.spec.ts).


Notes & Implementation details ⚠️

  • This library relies on TypeScript decorators; ensure tsconfig.json enables experimentalDecorators and emitDecoratorMetadata.
  • AggregateEvent assigns a GUID and recorded timestamp for each event.
  • The event emitter is fully encapsulated — consumers never interact with it directly. Use this.on(...) in registerEventHandlers() to subscribe, and applyEvent(...) / commit(...) to publish.
  • The AggregateRepository.save implementation iterates uncommitted events and calls handler.handle(...) when a handler's __handles matches the event __name (set by @Event).

Contributing & License

  • MIT © Matthew Krizanac
  • Contributions welcome; follow existing patterns in tests for examples.