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

@pickle-packs/journaling

v1.0.0

Published

Support for journals

Readme

Journaling

Append only journaling for application state. Every change is an entry. Current state is the result of replaying entries, optionally starting from a snapshot. Storage is not included. You plug in persistence.

Why use this

  • Full history for audits and debugging
  • Time travel by replaying to any point
  • Deterministic rebuilds for reproducibility
  • Safer writes since updates are append only
  • Easy evolution by adding new entry types
  • Fast loads with periodic snapshots
  • Portable because you bring your own storage

Core model

  • Entry: a small fact with an entryType.
  • Snapshot: a point-in-time materialized state with an entryNumber.
  • Journal: identity, state, ordered entries, and handler maps.
  • Handlers: pure functions that apply an entry or a snapshot to state.
  • Numbers and IDs: strong brands for EntryNumber, EntryType, and JournalId.
  • Outcomes: all operations return Outcome<T> to signal success or failure.

Quick start

  1. Define your entry types and validation.
  2. Implement line entry handlers that update state from an entry.
  3. Optionally implement snapshot handlers that restore state from a snapshot.
  4. Provide two persistence functions: loadEntries and saveEntries.
  5. Load a journal, apply entries, then save.

API surface

applyLineEntry(journal, entryOrEntries) -> Outcome<Journal>

  • Applies one or many entries in order.
  • Increments effectiveEntryNumber for each applied entry.
  • Appends the entry to journal.lineEntries.
  • Uses the handler mapped by entry.entryType.
  • Failure when no handler exists.

loadJournal(id, initialState, lineEntryHandlers, snapshotEntryHandlers, snapshotEntryInterval, loadEntries) -> Promise<Outcome<Journal>>

  • Calls your loadEntries to fetch { lineEntries, maybeSnapshotEntry }.
  • If a snapshot exists, restores from it, then replays remaining entries.
  • Returns a journal with empty lineEntries ready for new work.

saveJournal(journal, createSnapshot, saveEntries) -> Promise<Outcome<Journal>>

  • If effectiveEntryNumber - basisEntryNumber > snapshotEntryInterval, calls createSnapshot(journal) and includes it in Entries.
  • Calls your saveEntries.
  • On success, advances basisEntryNumber to effectiveEntryNumber and clears lineEntries.

Persistence contracts

type Entries<TState> = { lineEntries: Array<ILineEntry>, maybeSnapshotEntry: Maybe<ISnapshotEntry<TState>> }

type LoadEntries<TState> = (id: JournalId) => Promise<Entries<TState>>

type SaveEntries<TState> = (id: JournalId, entries: Entries<TState>, state: TState) => Promise<number>

You decide where and how to store data. Files, databases, object stores, or anything else are valid.

Snapshotting

Use SnapshotEntryInterval to bound rebuild time. When the interval is exceeded, saveJournal requests a snapshot via your createSnapshot function. Snapshots keep loads fast while the journal stays append only.

Failures

  • ENTRY_HANDLER_NOT_SPECIFIED
  • LOAD_JOURNAL_FAILURE
  • SAVE_JOURNAL_FAILURE

Failures carry a detail message and an optional maybeError.

Design notes

  • Pure, functional handlers
  • Append only updates
  • Deterministic projections
  • In memory engine with user supplied persistence
  • Strongly branded identifiers to avoid mixups

Counter Journal Example

A minimal step by step guide that mirrors the CounterJournal test module. This explains how to implement a simple journal using this library.

1) Define your line entry and entry type

const counterValueAddedV1 = "acme.counter-value-added.v1";

type CounterValue = number & { readonly __brand: "CounterValue" };

interface ICounterValueAddedV1LineEntry extends ILineEntry {
  readonly entryType: EntryType;        // should be counterValueAddedV1
  readonly value: CounterValue;
}

2) Define the handler that mutates state when the entry is processed

Handlers are pure. They receive current state and a line entry. They return the next state. This is where state is mutated as entries are replayed.

type CounterState = Readonly<{
  average: CounterValue;
  maximum: CounterValue;
  minimum: CounterValue;
  valueCount: number;
}>;

function handleCounterValueAddedV1(
  state: Readonly<CounterState>,
  entryLike: Readonly<ILineEntry>
): CounterState {
  const entry = entryLike as ICounterValueAddedV1LineEntry;
  const nextCount = state.valueCount + 1;
  return {
    average: Math.round(((state.average * state.valueCount) + entry.value) / nextCount) as CounterValue,
    maximum: entry.value > state.maximum ? entry.value : state.maximum,
    minimum: entry.value < state.minimum ? entry.value : state.minimum,
    valueCount: nextCount
  };
}

Register the handler:

const lineEntryHandlers: Record<EntryType, LineEntryHandler<CounterState>> = {
  [counterValueAddedV1]: handleCounterValueAddedV1
};

3) Expose an action that decides whether to append an entry

This is the action boundary. It runs business rules against the current state to decide if an entry should be appended. It returns Outcome<Journal> and never mutates state directly. Invalid actions return a failure outcome.

function addValue(
  journal: CounterJournal,
  value: CounterValue
): Outcome<CounterJournal> {
  // Business rules first
  if (value < 0) {
    return failure({
      code: "NEGATIVE_COUNTER_VALUE",
      detail: "Value must be non-negative",
      maybeError: none
    });
  }

  // Build the entry
  const lineEntry: ICounterValueAddedV1LineEntry = {
    entryType: counterValueAddedV1 as EntryType,
    value
  };

  // Optional validation layer can run here before append

  // Append via applyLineEntry which will call the handler
  return pipe(
    success(journal),
    (j) => applyLineEntry<CounterState, CounterJournal, ILineEntry>(j, lineEntry)
  );
}

4) Snapshots

A snapshot is a persisted aggregate used as a starting point to reduce load time when journals grow large. Choose an interval that keeps hydration within your target. Smaller is not always better. Tune based on acceptable load time.

Define a snapshot type and interval, and a function to create a snapshot from a journal:

const counterJournalSnapshotV1 = "acme.counter-journal-snapshot.v1";
const snapshotInterval: SnapshotEntryInterval = 5 as SnapshotEntryInterval;

function createSnapshot(j: Journal<CounterState>): ISnapshotEntry<CounterState> {
  return {
    entryNumber: j.effectiveEntryNumber,
    entryType: counterJournalSnapshotV1 as EntryType,
    state: j.state
  };
}

const snapshotEntryHandlers: Record<EntryType, SnapshotEntryHandler<CounterState, ISnapshotEntry<CounterState>>> = {
  [counterJournalSnapshotV1 as EntryType]: (snap) => snap.state
};

Snapshots are requested during saveJournal only when the interval is exceeded. Only one snapshot is included per save.

5) Persistence hooks

You provide two functions. The persistence mechanism must preserve ordering and should assign monotonically increasing entry numbers. The library replays in the order returned.

async function loadEntries(id: JournalId): Promise<Entries<CounterState>> {
  // Choose the latest snapshot
  const latestSnapshot = maybe(
    [...(snapshotEntryStore.get(id) ?? [])]
      .sort((a, b) => b.entryNumber - a.entryNumber)[0]
  );

  // Fetch entries after the snapshot, or all if none
  const lineEntries = match(
    latestSnapshot,
    (snap) => [...(lineEntryStore.get(id) ?? [])].slice(snap.entryNumber),
    () => [...(lineEntryStore.get(id) ?? [])]
  );

  return {
    lineEntries,
    maybeSnapshotEntry: latestSnapshot
  };
}

async function saveEntries(
  id: JournalId,
  entries: Entries<CounterState>,
  _state: Readonly<CounterState>
): Promise<number> {
  // Persist optional snapshot
  const snapshots = match(
    entries.maybeSnapshotEntry,
    (snap) => [ ...(snapshotEntryStore.get(id) ?? []), snap ],
    []
  );
  snapshotEntryStore.set(id, snapshots);

  // Append line entries in order
  const prior = lineEntryStore.get(id) ?? [];
  lineEntryStore.set(id, [ ...prior, ...entries.lineEntries ]);

  return Promise.resolve(1 + entries.lineEntries.length);
}

6) Wiring load and save

const initialCounterState: CounterState = {
  average: 0 as CounterValue,
  maximum: 0 as CounterValue,
  minimum: 99999 as CounterValue,
  valueCount: 0
};

export type CounterJournal = Journal<CounterState> & { readonly __brand: "CounterJournal" };

async function load(id: JournalId): Promise<Outcome<CounterJournal>> {
  return loadJournal<CounterState, CounterJournal>(
    id,
    initialCounterState,
    lineEntryHandlers,
    snapshotEntryHandlers,
    snapshotInterval,
    loadEntries
  );
}

async function save(journal: CounterJournal): Promise<Outcome<CounterJournal>> {
  return saveJournal<CounterState, CounterJournal>(
    journal,
    createSnapshot,
    saveEntries
  );
}

7) Multiple handlers and forward only change

Handler maps support many entry types. Use versions like v1, v2, v3 in the type name to evolve behavior without rewriting history. New behavior is introduced by new entry types. Old entries remain valid. This supports forward only change and backward compatibility.

const handlers = {
  "acme.counter-value-added.v1": handleCounterValueAddedV1,
  // add future types here
};

You now have a complete loop:

  • An action function decides whether to append an entry.
  • applyLineEntry records the entry and mutates state via the handler.
  • saveJournal persists entries and optionally a snapshot depending on the interval.
  • loadJournal restores from an optional snapshot and replays entries in order.