
Company News
Socket Named Top Sales Organization by RepVue
Socket won two 2026 Reppy Awards from RepVue, ranking in the top 5% of all sales orgs. AE Alexandra Lister shares what it's like to grow a sales career here.
@pickle-packs/journaling
Advanced tools
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.
entryType.entryNumber.EntryNumber, EntryType, and JournalId.Outcome<T> to signal success or failure.loadEntries and saveEntries.applyLineEntry(journal, entryOrEntries) -> Outcome<Journal>
effectiveEntryNumber for each applied entry.journal.lineEntries.entry.entryType.loadJournal(id, initialState, lineEntryHandlers, snapshotEntryHandlers, snapshotEntryInterval, loadEntries) -> Promise<Outcome<Journal>>
loadEntries to fetch { lineEntries, maybeSnapshotEntry }.lineEntries ready for new work.saveJournal(journal, createSnapshot, saveEntries) -> Promise<Outcome<Journal>>
effectiveEntryNumber - basisEntryNumber > snapshotEntryInterval, calls createSnapshot(journal) and includes it in Entries.saveEntries.basisEntryNumber to effectiveEntryNumber and clears lineEntries.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.
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.
ENTRY_HANDLER_NOT_SPECIFIEDLOAD_JOURNAL_FAILURESAVE_JOURNAL_FAILUREFailures carry a detail message and an optional maybeError.
A minimal step by step guide that mirrors the CounterJournal test module. This explains how to implement a simple journal using this library.
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;
}
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
};
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)
);
}
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.
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);
}
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
);
}
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:
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.FAQs
Support for journals
We found that @pickle-packs/journaling demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket won two 2026 Reppy Awards from RepVue, ranking in the top 5% of all sales orgs. AE Alexandra Lister shares what it's like to grow a sales career here.

Security News
NIST will stop enriching most CVEs under a new risk-based model, narrowing the NVD's scope as vulnerability submissions continue to surge.

Company News
/Security News
Socket is an initial recipient of OpenAI's Cybersecurity Grant Program, which commits $10M in API credits to defenders securing open source software.