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

opossum-js

v0.1.0

Published

File system-based event store for TypeScript/Bun implementing the DCB pattern

Readme

opossum-js

A file system-based event store for TypeScript/Bun implementing the DCB (Dynamic Consistency Boundaries) pattern.

TypeScript port of Opossum by Martin Tibor Major.

What is opossum-js?

opossum-js is a lightweight, file system-based event store that requires no external database or infrastructure. It stores domain events as JSON files on disk with tag-based indexing, optimistic concurrency control, and built-in projection support.

  • Zero infrastructure — no database, no message broker, just the file system
  • Full audit trail — every state change is captured as an immutable event
  • Local data ownership — all data stays on the local machine
  • Optimistic concurrency — append conditions prevent conflicting writes
  • Tag-based indexing — flexible event categorization and querying
  • Projections — materialized views with incremental updates and background polling
  • DCB pattern — Dynamic Consistency Boundaries via AppendCondition and Query

When to Use

  • Point-of-sale (POS) systems — offline-capable with local event storage
  • Field service apps — works without network connectivity
  • Small/medium business tools — simple deployment, no ops overhead
  • Data sovereignty — keep data on-premises or on the user's device
  • Development and testing — fast iteration without database setup
  • Prototyping event-sourced systems — validate domain models before scaling

When NOT to Use

  • Distributed systems — no built-in replication or clustering
  • High throughput — file I/O has inherent limits compared to dedicated databases
  • Cloud-native architectures — better served by managed event stores
  • Multi-region deployments — no cross-node coordination

Quick Start

import { createEventStore, toNewEvent, Query } from 'opossum-js';

// Create a store (writes to ./OpossumStore/my-store/)
const store = await createEventStore({ storeName: 'my-store' });

// Append events
await store.append([
  toNewEvent('ItemAdded', { itemId: 'item-1', name: 'Widget', price: 9.99 }, [
    { key: 'itemId', value: 'item-1' },
  ]),
  toNewEvent('ItemAdded', { itemId: 'item-2', name: 'Gadget', price: 19.99 }, [
    { key: 'itemId', value: 'item-2' },
  ]),
]);

// Query events by type
const query = Query.fromEventTypes('ItemAdded');
const events = await store.read(query);

console.log(`Found ${events.length} events`);
for (const e of events) {
  console.log(`  [${e.position}] ${e.event.eventType}:`, e.event.event);
}

Core Concepts

Events

Events are the fundamental unit of storage. There are three event shapes:

  • DomainEvent — the event payload with a type name and tags:

    interface DomainEvent {
      readonly eventType: string;
      readonly event: IEvent;       // { [key: string]: unknown }
      readonly tags: readonly Tag[];
    }
  • NewEvent — what you pass to append(), wrapping a DomainEvent with optional metadata:

    interface NewEvent {
      readonly event: DomainEvent;
      readonly metadata?: Metadata;
    }
  • SequencedEvent — what you get back from read(), with a global sequence position:

    interface SequencedEvent {
      readonly event: DomainEvent;
      readonly position: number;
      readonly metadata: Metadata;
    }

Tags

Tags are key-value pairs attached to events for categorization and querying:

interface Tag {
  readonly key: string;
  readonly value: string;
}

Tags enable filtering events by entity, aggregate, or any custom dimension without needing dedicated streams.

Queries

Queries define which events to match. They support event type filtering, tag filtering, or both:

// All events
Query.all()

// Events by type
Query.fromEventTypes('OrderPlaced', 'OrderShipped')

// Events by tag
Query.fromTags({ key: 'orderId', value: 'order-123' })

// Combined: specific types with specific tags
Query.fromItems(
  { eventTypes: ['OrderPlaced'], tags: [{ key: 'customerId', value: 'c-1' }] },
  { eventTypes: ['PaymentReceived'] },
)

Multiple query items are OR-combined: an event matches if it satisfies any item.

AppendCondition

Append conditions implement optimistic concurrency control. They specify a query and a sequence position — the append fails if any matching event has been written after that position:

interface AppendCondition {
  readonly failIfEventsMatch: Query;
  readonly afterSequencePosition?: number;
}

This is the core mechanism behind the DCB pattern.

Configuration

Create a store with createEventStore(). Only storeName is required; everything else has sensible defaults:

const store = await createEventStore({
  storeName: 'my-store',
  rootPath: 'OpossumStore',              // default
  flushEventsImmediately: true,          // default
  writeProtectEventFiles: false,         // default
  writeProtectProjectionFiles: false,    // default
  crossProcessLockTimeoutMs: 5000,       // default
});

| Option | Type | Default | Description | |---|---|---|---| | storeName | string | (required) | Name of the store (used as directory name) | | rootPath | string | 'OpossumStore' | Root directory for all store data | | flushEventsImmediately | boolean | true | Flush events to disk after each write | | writeProtectEventFiles | boolean | false | Set event files to read-only after writing | | writeProtectProjectionFiles | boolean | false | Set projection files to read-only after writing | | crossProcessLockTimeoutMs | number | 5000 | Timeout for cross-process file locks |

API Reference

EventStore

The main interface for reading and writing events:

interface EventStore {
  append(events: NewEvent[], condition?: AppendCondition, signal?: AbortSignal): Promise<void>;
  read(query: Query, options?: {
    readOptions?: ReadOption[];
    fromPosition?: number;
    maxCount?: number;
  }): Promise<SequencedEvent[]>;
  readLast(query: Query, signal?: AbortSignal): Promise<SequencedEvent | undefined>;
}

EventStoreAdmin

Administrative operations:

interface EventStoreAdmin {
  deleteStore(signal?: AbortSignal): Promise<void>;
}

EventStoreMaintenance

Maintenance operations for evolving tag schemas:

interface EventStoreMaintenance {
  addTags(
    eventType: string,
    tagFactory: (event: SequencedEvent) => readonly Tag[],
    signal?: AbortSignal,
  ): Promise<TagMigrationResult>;
}

Helper Functions

Convenience functions from opossum-js:

// Create a NewEvent from parts
toNewEvent(eventType: string, event: IEvent, tags?: Tag[], metadata?: Metadata): NewEvent

// Append a single event
appendOne(store: EventStore, event: NewEvent, condition?: AppendCondition, signal?: AbortSignal): Promise<void>

// Append events without a condition
appendEvents(store: EventStore, events: NewEvent[], signal?: AbortSignal): Promise<void>

// Read all matching events
readAll(store: EventStore, query: Query): Promise<SequencedEvent[]>

// Read events from a position
readFrom(store: EventStore, query: Query, fromPosition: number): Promise<SequencedEvent[]>

DomainEventBuilder

Fluent builder for constructing events with metadata:

import { DomainEventBuilder } from 'opossum-js';

const event = new DomainEventBuilder(
  { orderId: 'order-1', total: 42.00 },
  'OrderPlaced',
)
  .withTag('orderId', 'order-1')
  .withCorrelationId('req-abc')
  .withUserId('user-42')
  .build();

Decision Models

Decision models combine a read (projection) and a write (append with condition) into an atomic decision. They are the primary way to enforce business rules with the DCB pattern.

DecisionProjection

A decision projection defines what state to build from events:

import { createDecisionProjection, Query } from 'opossum-js';

const orderTotal = createDecisionProjection({
  initialState: 0,
  query: Query.fromEventTypes('ItemAdded', 'ItemRemoved'),
  apply: (state, event) => {
    if (event.event.eventType === 'ItemAdded') return state + (event.event.event as any).price;
    if (event.event.eventType === 'ItemRemoved') return state - (event.event.event as any).price;
    return state;
  },
});

Building Decision Models

import { buildDecisionModel } from 'opossum-js';

const { state, appendCondition } = await buildDecisionModel(store, orderTotal);

// state = current total
// appendCondition = ensures no conflicting events since we read

if (state < 100) {
  await store.append(
    [toNewEvent('ItemAdded', { price: 25.00 })],
    appendCondition,
  );
}

For multiple projections over a single event read:

import { buildDecisionModel2, buildDecisionModel3, buildDecisionModelN } from 'opossum-js';

// Two projections
const { first, second, appendCondition } = await buildDecisionModel2(store, projA, projB);

// Three projections
const { first, second, third, appendCondition } = await buildDecisionModel3(store, projA, projB, projC);

// N projections (same type)
const { states, appendCondition } = await buildDecisionModelN(store, [projA, projB, projC]);

executeDecision

Retry helper that automatically retries on AppendConditionFailedError with exponential backoff:

import { executeDecision } from 'opossum-js';

const result = await executeDecision(store, async (s) => {
  const { state, appendCondition } = await buildDecisionModel(s, orderTotal);

  if (state >= 100) return { success: false, reason: 'limit reached' };

  await s.append(
    [toNewEvent('ItemAdded', { price: 25.00 })],
    appendCondition,
  );
  return { success: true };
}, {
  maxRetries: 3,        // default
  initialDelayMs: 50,   // default, doubles each retry
});

Projections

Projections build materialized views from events, stored as JSON files on disk.

ProjectionDefinition

Define how events map to read-model state:

import type { ProjectionDefinition } from 'opossum-js';

interface OrderSummary {
  orderId: string;
  itemCount: number;
  total: number;
}

const orderSummaryProjection: ProjectionDefinition<OrderSummary> = {
  name: 'order-summary',
  eventTypes: ['ItemAdded', 'ItemRemoved'],
  keySelector: (event) => event.event.tags.find(t => t.key === 'orderId')?.value ?? 'unknown',
  apply: (state, event) => {
    const current = state ?? { orderId: 'unknown', itemCount: 0, total: 0 };
    if (event.event.eventType === 'ItemAdded') {
      return {
        ...current,
        orderId: (event.event.event as any).orderId ?? current.orderId,
        itemCount: current.itemCount + 1,
        total: current.total + ((event.event.event as any).price ?? 0),
      };
    }
    return current;
  },
};

ProjectionManager

Manages registration and incremental updates of projections:

import { ProjectionManager } from 'opossum-js';

const manager = new ProjectionManager({
  eventStore: store,
  basePath: './OpossumStore/my-store',
});

manager.register(orderSummaryProjection);

// Process new events
const events = await store.read(Query.all(), { fromPosition: lastPosition });
await manager.update(events);

// Read projection state
const projectionStore = manager.getStore<OrderSummary>('order-summary');
const order = await projectionStore.get('order-123');

ProjectionDaemon

Background polling that automatically updates projections as new events arrive:

import { ProjectionDaemon } from 'opossum-js';

const daemon = new ProjectionDaemon(manager, {
  pollingIntervalMs: 5000,   // default
  batchSize: 1000,           // default
  onError: (err) => console.error('Projection error:', err),
});

daemon.start();
// ... later
daemon.stop();

ProjectionRebuilder

Rebuild projections from scratch (e.g., after schema changes):

import { ProjectionRebuilder } from 'opossum-js';

const rebuilder = new ProjectionRebuilder(manager, {
  batchSize: 5000,
  flushInterval: 10000,
});

// Rebuild a single projection
const result = await rebuilder.rebuild('order-summary');
console.log(`Rebuilt in ${result.durationMs}ms, processed ${result.eventsProcessed} events`);

// Rebuild all projections
await rebuilder.rebuildAll({ force: true });

ProjectionStore

Read and query projection state:

interface ProjectionStore<TState> {
  get(key: string): Promise<TState | undefined>;
  getAll(): Promise<Map<string, TState>>;
  save(key: string, state: TState): Promise<void>;
  delete(key: string): Promise<void>;
  queryByTag(tag: Tag): Promise<Map<string, TState>>;
  queryByTags(tags: readonly Tag[]): Promise<Map<string, TState>>;
}

The DCB Pattern

opossum-js implements the Dynamic Consistency Boundaries pattern. Instead of grouping events into fixed aggregates, you define consistency boundaries dynamically through queries:

  1. Read relevant events using a Query (by event type, tags, or both)
  2. Decide based on the current state (built by folding events)
  3. Append new events with an AppendCondition that references the same query and the last-seen position

If a conflicting event was written between your read and append, the store throws AppendConditionFailedError. Use executeDecision() to automatically retry.

This approach means a single event can participate in multiple consistency boundaries, and boundaries can evolve without restructuring your event streams.

Storage Format

opossum-js stores data under {rootPath}/{storeName}/:

OpossumStore/
  my-store/
    events/
      000000001.json      # sequenced event files
      000000002.json
      ...
    indices/
      event-types/        # event type index
      tags/               # tag index
      positions/          # position index
    projections/
      order-summary/      # projection state files
      _checkpoints/       # projection checkpoint files
    ledger.json           # global sequence counter

Event files are human-readable JSON containing the domain event, metadata, and sequence position.

Limitations

  • Bun runtime only — uses Bun-specific APIs; does not run on Node.js or Deno
  • Not distributed — single-machine, single-process (with cross-process locking)
  • No OpenTelemetry — no built-in observability instrumentation yet
  • File system performance — throughput is bounded by disk I/O

License

MIT

Acknowledgments

opossum-js is a TypeScript/Bun port of Opossum by Martin Tibor Major. The original project is a .NET file system-based event store implementing the DCB pattern. All credit for the architecture and design goes to the original author.

Learn more about the DCB pattern at dcb.events.