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

observator

v0.1.2

Published

Type-safe observable store that emits events for each top-level field change with JSON Patch arrays.

Readme

observator

A type-safe store that emits events for each top-level field change. Uses patch-recorder by default for mutative updates with JSON Patch generation, but you can use any compatible library (e.g., mutative or immer). Uses radiate for type-safe event emission.

Features

  • 🔒 Type-safe event names - Only valid field names can be used for updates and subscriptions
  • 📝 Patches included - Event callbacks receive JSON Patch arrays
  • 🎯 Patch mechanism agnostic - Use any patch generation library (patch-recorder, mutative, immer, etc.)
  • 🎯 Fine-grained subscriptions - Subscribe to specific keys within Record/Map fields using keyed events
  • 💡 Value-based subscriptions - Use the convenient subscribe API to receive current values immediately and on every change
  • 🎯 Minimal events - Events are emitted only for the specific field being updated
  • 📦 Minimal dependencies - Only depends on patch-recorder and radiate
  • 🚀 Lightweight - Small footprint with powerful features

Installation

npm install observator
# or
pnpm add observator
# or
yarn add observator

By default, observator uses patch-recorder for patch generation. To use a different library, install it as well:

# Using mutative
npm install mutative

# Using immer
npm install immer

Usage

Quick Start

The simplest way to use observe-store is to import createObservableStore and call it with your initial state. It uses patch-recorder by default:

import {createObservableStore} from 'observator';

type State = {
  counter: number;
  user: { name: string };
};

const store = createObservableStore<State>({
  counter: 0,
  user: { name: 'John' }
});

// Subscribe to counter updates
store.on('counter:updated', (patches) => {
  console.log('Counter changed:', patches);
});

// Update counter
store.update((state) => {
  state.counter += 1;
});

console.log(store.get('counter')); // 1

Basic Example with Primitives

import {createObservableStore} from 'observator';

type State = {
  counter: number;
  name: string;
};

const store = createObservableStore<State>({
  counter: 0,
  name: 'John'
});

// Subscribe to counter updates
store.on('counter:updated', (patches) => {
  console.log('Counter changed:', patches);
  // Output: [{ op: 'replace', path: ['counter'], value: 1 }]
});

// Update counter
store.update((state) => {
  state.counter += 1;
});

console.log(store.get('counter')); // 1

Example with Complex Objects

import {createObservableStore} from 'observator';

type State = {
  user: {
    name: string;
    age: number;
    email: string;
  };
  settings: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
};

const store = createObservableStore<State>({
  user: {
    name: 'John Doe',
    age: 30,
    email: '[email protected]'
  },
  settings: {
    theme: 'light',
    notifications: true
  }
});

// Subscribe to user changes
store.on('user:updated', (patches) => {
  console.log('User updated:', patches);
  // Output: [{ op: 'replace', path: ['user', 'name'], value: 'Jane Doe' }]
});

// Update user
store.update((state) => {
  state.user.name = 'Jane Doe';
  state.user.age = 31;
});

// Subscribe to settings changes
const unsubscribe = store.on('settings:updated', (patches) => {
  console.log('Settings changed:', patches);
});

// Update settings
store.update((state) => {
  state.settings.theme = 'dark';
  state.settings.notifications = false;
});

// Unsubscribe later
unsubscribe();

Value-based Subscriptions

The subscribe API provides a convenient way to subscribe to field values. Unlike the patch-based on() API, subscribe:

  • Executes immediately with the current value
  • Provides the full value on every change (not just patches)
  • Returns an unsubscribe function for easy cleanup
import {createObservableStore} from 'observator';

type State = {
  counter: number;
  user: { name: string };
};

const store = createObservableStore<State>({
  counter: 0,
  user: { name: 'John' }
});

// Subscribe to counter - callback fires immediately with current value
store.subscriptions.counter((counter) => {
  console.log('Counter value:', counter);
});
// Output: Counter value: 0

// Update counter - callback fires again with new value
store.update((state) => {
  state.counter += 1;
});
// Output: Counter value: 1

// Subscribe to user
const unsubscribe = store.subscriptions.user((user) => {
  console.log('User name:', user.name);
});
// Output: User name: John

// Unsubscribe from user updates
unsubscribe();

store.update((state) => {
  state.user.name = 'Jane';
});
// No callback fired (unsubscribed)

Benefits of Value-based Subscriptions

  • Simpler API: No need to manually get current values or apply patches
  • Immediate execution: Always receive the current value right away
  • Cleaner code: Less boilerplate compared to patch-based subscriptions
  • Same type safety: Full TypeScript support with field name inference

Comparison: Patch-based vs Value-based

// Patch-based subscription
store.on('counter:updated', (patches) => {
  const current = store.get('counter');
  console.log('Counter:', current);
  // Need to manually get current value
});

// Value-based subscription
store.subscriptions.counter((counter) => {
  console.log('Counter:', counter);
  // Automatically receives latest value
});

Example with Arrays

import {createObservableStore} from 'observator';

type State = {
  items: number[];
  todos: Array<{ id: number; text: string; done: boolean }>;
};

const store = createObservableStore<State>({
  items: [1, 2, 3],
  todos: [
    { id: 1, text: 'Learn TypeScript', done: false }
  ]
});

// Subscribe to items changes
store.on('items:updated', (patches) => {
  console.log('Items changed:', patches);
});

// Add item
store.update((state) => {
  state.items.push(4);
});

// Update todos
store.update((state) => {
  state.todos.push({ id: 2, text: 'Build apps', done: false });
  state.todos[0].done = true;
});

Wildcard Event - Subscribe to All Updates

Subscribe to all updates across the entire store using the wildcard '*' event:

import {createObservableStore} from 'observator';

type State = {
  counter: number;
  user: { name: string };
  settings: { theme: string };
};

const store = createObservableStore<State>({
  counter: 0,
  user: { name: 'John' },
  settings: { theme: 'light' }
});

// Subscribe to all updates
const unsubscribe = store.on('*', (patches) => {
  console.log('State changed with patches:', patches);
});

// Update counter - wildcard event fires
store.update((state) => {
  state.counter += 1;
});
// Output: State changed with patches: [{ op: 'replace', path: ['counter'], value: 1 }]

// Update user - wildcard event fires again
store.update((state) => {
  state.user.name = 'Jane';
});
// Output: State changed with patches: [{ op: 'replace', path: ['user', 'name'], value: 'Jane' }]

// Update multiple fields at once - wildcard event fires once with all patches
store.update((state) => {
  state.counter += 1;
  state.settings.theme = 'dark';
});
// Output: State changed with patches: [
//   { op: 'replace', path: ['counter'], value: 2 },
//   { op: 'replace', path: ['settings', 'theme'], value: 'dark' }
// ]

unsubscribe();

Use Cases

The wildcard event is useful for:

  • Logging/debugging - Track all state changes
  • Persistence - Save state to localStorage/database on any change
  • Analytics - Track user interactions across the app
  • Undo/redo systems - Maintain a history of all changes

Combining with Field-Specific Events

You can use the wildcard event alongside field-specific events:

// Wildcard listener for logging
store.on('*', (patches) => {
  console.log('State changed:', patches);
});

// Field-specific listener for specific logic
store.on('user:updated', (patches) => {
  console.log('User specifically changed:', patches);
});

store.update((state) => {
  state.user.name = 'Jane';
});
// Both listeners fire

Single Emission

Subscribe to all updates for a single emission only:

// Subscribe for single emission
store.once('*', (patches) => {
  console.log('State changed once:', patches);
});

store.update((state) => {
  state.counter += 1;
});
// Callback fires once

store.update((state) => {
  state.counter += 1;
});
// Callback does NOT fire again

Unsubscribe Specific Listener

Remove a specific wildcard listener:

const callback1 = (patches) => console.log('Listener 1:', patches);
const callback2 = (patches) => console.log('Listener 2:', patches);

store.on('*', callback1);
store.on('*', callback2);

store.update((state) => {
  state.counter += 1;
});
// Both callbacks fire

store.off('*', callback1);

store.update((state) => {
  state.counter += 1;
});
// Only callback2 fires

Keyed Events - Subscribe to Specific Keys

For fields containing records or arrays, you can subscribe to changes for specific keys:

import {createObservableStore} from 'observator';

type State = {
  users: Record<string, { name: string; email: string }>;
};

const store = createObservableStore<State>({
  users: {
    'user-1': { name: 'John', email: '[email protected]' },
    'user-2': { name: 'Jane', email: '[email protected]' }
  }
});

// Subscribe to specific user changes
const unsubscribe1 = store.onKeyed('users:updated', 'user-1', (patches) => {
  console.log('User 1 changed:', patches);
});

// Update user-1
store.update((state) => {
  state.users['user-1'].name = 'Johnny';
});
// Only the user-1 callback fires

// Update user-2
store.update((state) => {
  state.users['user-2'].name = 'Janet';
});
// The user-1 callback does NOT fire

unsubscribe1();

Wildcard Subscription

Subscribe to all keys in a field using the wildcard '*':

// Subscribe to all user changes
const unsubscribe = store.onKeyed('users:updated', '*', (userId, patches) => {
  console.log(`User ${userId} changed:`, patches);
});

store.update((state) => {
  state.users['user-1'].name = 'Johnny';
  state.user['user-2'].name = 'Janet';
});

unsubscribe();

Array Index Subscription

Subscribe to specific array indices:

type State = {
  todos: Array<{ id: number; text: string; done: boolean }>;
};

const store = createObservableStore<State>({
  todos: [
    { id: 1, text: 'Task 1', done: false },
    { id: 2, text: 'Task 2', done: false }
  ]
});

// Subscribe to first todo changes
store.onKeyed('todos:updated', 0, (patches) => {
  console.log('First todo changed:', patches);
});

store.update((state) => {
  state.todos[0].done = true;
});
// Only the first todo callback fires

Single Emission

Subscribe for a single event using onceKeyed:

// Subscribe for single emission
store.onceKeyed('users:updated', 'user-1', (patches) => {
  console.log('User 1 changed once:', patches);
});

store.update((state) => {
  state.users['user-1'].name = 'Johnny';
});
// Callback fires once

store.update((state) => {
  state.users['user-1'].name = 'John';
});
// Callback does NOT fire again

Unsubscribe Specific Listener

Remove a specific listener from a keyed event:

const callback1 = (patches) => console.log('Callback 1:', patches);
const callback2 = (patches) => console.log('Callback 2:', patches);

store.onKeyed('users:updated', 'user-1', callback1);
store.onKeyed('users:updated', 'user-1', callback2);

store.update((state) => {
  state.users['user-1'].name = 'Johnny';
});
// Both callbacks fire

store.offKeyed('users:updated', 'user-1', callback1);

store.update((state) => {
  state.users['user-1'].name = 'John';
});
// Only callback2 fires

Multiple Subscribers

import {createObservableStore} from 'observator';

type State = {
  count: number;
};

const store = createObservableStore<State>({
  count: 0
});

// Multiple subscribers to the same event
const unsubscribe1 = store.on('count:updated', (patches) => {
  console.log('Subscriber 1:', patches);
});

const unsubscribe2 = store.on('count:updated', (patches) => {
  console.log('Subscriber 2:', patches);
});

store.update((state) => {
  state.count += 1;
});

// Both subscribers receive the event
// Output:
// Subscriber 1: [{ op: 'replace', path: ['value'], value: 1 }]
// Subscriber 2: [{ op: 'replace', path: ['value'], value: 1 }]

Get Entire State

import {createObservableStore} from 'observator';

type State = {
  user: { name: string };
  counter: number;
};

const store = createObservableStore<State>({
  user: { name: 'John' },
  counter: 0
});

const entireState = store.getState();
console.log(entireState);
// { user: { name: 'John' }, counter: 0 }

// Returns a shallow copy, so modifications don't affect the store
const stateCopy = store.getState();
stateCopy.counter = 999;
console.log(store.get('counter')); // Still 0

API

createObservableStore<T>(state: T, options?: ObservableStoreOptions): ObservableStore<T>

Creates a new ObservableStore instance with the given initial state. Uses patch-recorder by default, but you can pass a custom create function.

Type Parameter:

  • T - The state type, must be Record<string, unknown> & NonPrimitive

Parameters:

  • state - Initial state object
  • options - Optional configuration object
    • createFunction?: CreateFunction - Custom create function for patch generation

Returns:

  • A new ObservableStore<T> instance

Example (default usage):

import {createObservableStore} from 'observator';

const store = createObservableStore({
  user: { name: 'John' },
  counter: 0
});

Example (with custom create function):

import {createObservableStore} from 'observator';
import {create} from 'mutative';

const store = createObservableStore(
  {
    user: { name: 'John' },
    counter: { value: 0 }
  },
  {createFunction: (state, mutate) => create(state, mutate, {enablePatches: true})},
);

ObservableStore<T>

update(mutate: (state: T) => void): Patches

Updates the full state and emits events for each field that changed.

Parameters:

  • mutate - Mutation function that receives the full state

Returns:

  • Array of patches for the full state

Example:

store.update((state) => {
  state.user.name = 'Jane';
});

get<K extends keyof T>(name: K): T[K]

Gets the current value of a field.

Type Parameters:

  • K - The field key to retrieve

Parameters:

  • name - The field key to retrieve

Returns:

  • The current value of the field

Example:

const user = store.get('user');
console.log(user.name); // 'John'

on<K extends keyof T>(event: EventName<K>, callback: (patches: Patches) => void): () => void

Subscribes to updates for a specific field.

Type Parameters:

  • K - The field key to subscribe to

Parameters:

  • event - The event name in format ${fieldName}:updated
  • callback - Callback function that receives the patches array

Returns:

  • Unsubscribe function

Example:

const unsubscribe = store.on('user:updated', (patches) => {
  console.log('User changed:', patches);
});

// Later: unsubscribe();
unsubscribe();

off<K extends keyof T>(event: EventName<K>, callback: (patches: Patches) => void): void

Removes a specific event listener.

Type Parameters:

  • K - The field key

Parameters:

  • event - The event name in format ${fieldName}:updated
  • callback - The exact callback function to remove

Example:

const callback = (patches) => console.log('User changed:', patches);
store.on('user:updated', callback);

// Later:
store.off('user:updated', callback);

once<K extends keyof T>(event: EventName<K>, callback: (patches: Patches) => void): () => void

Subscribes to a single emission of an event.

Type Parameters:

  • K - The field key to subscribe to

Parameters:

  • event - The event name in format ${fieldName}:updated
  • callback - Callback function that receives the patches array

Returns:

  • Unsubscribe function to remove listener before it fires

Example:

const unsubscribe = store.once('user:updated', (patches) => {
  console.log('User changed once:', patches);
});

// Callback will fire once, then automatically unsubscribe

on(event: EventNames<T>, callback: (patches: Patches) => void): () => void

Subscribes to updates for a specific field or all updates.

Parameters:

  • event - The event name in format ${fieldName}:updated or '*' for all updates
  • callback - Callback function that receives the patches array

Returns:

  • Unsubscribe function

Examples:

// Subscribe to field-specific updates
const unsubscribe = store.on('user:updated', (patches) => {
  console.log('User changed:', patches);
});

// Subscribe to all updates (wildcard)
const unsubscribeAll = store.on('*', (patches) => {
  console.log('State updated:', patches);
});

off(event: EventNames<T>, callback: (patches: Patches) => void): void

Unsubscribes from an event.

Parameters:

  • event - The event name in format ${fieldName}:updated or '*'
  • callback - The exact callback function to remove

Examples:

const callback = (patches) => console.log('Updated:', patches);
store.on('user:updated', callback);

// Later:
store.off('user:updated', callback);

// Or for wildcard:
store.off('*', callback);

once(event: EventNames<T>, callback: (patches: Patches) => void): () => void

Subscribes to an event for a single emission only.

Parameters:

  • event - The event name in format ${fieldName}:updated or '*' for all updates
  • callback - Callback function that receives the patches array

Returns:

  • Unsubscribe function to remove listener before it fires

Examples:

// Subscribe for single emission to specific field
const unsubscribe = store.once('user:updated', (patches) => {
  console.log('User changed once:', patches);
});

// Subscribe for single emission to all updates
const unsubscribeAll = store.once('*', (patches) => {
  console.log('State updated once:', patches);
});

onKeyed<K extends keyof T>(event: EventName<K>, key: Key, callback: (patches: Patches) => void): () => void

Subscribes to updates for a specific key within a field.

Type Parameters:

  • K - The field key to subscribe to

Parameters:

  • event - The event name in format ${fieldName}:updated
  • key - The specific key to listen for (e.g., user ID, array index)
  • callback - Callback function that receives the patches array

Returns:

  • Unsubscribe function

Example:

const unsubscribe = store.onKeyed('users:updated', 'user-123', (patches) => {
  console.log('User 123 changed:', patches);
});

unsubscribe();

onKeyed<K extends keyof T>(event: EventName<K>, key: '*', callback: (key: ExtractKeyType<T[K]>, patches: Patches) => void): () => void

Subscribes to all keys within a field (wildcard subscription).

Type Parameters:

  • K - The field key to subscribe to

Parameters:

  • event - The event name in format ${fieldName}:updated
  • key - Use '*' to listen to all keys
  • callback - Callback function that receives the key and patches array

Returns:

  • Unsubscribe function

Example:

const unsubscribe = store.onKeyed('users:updated', '*', (userId, patches) => {
  console.log(`User ${userId} changed:`, patches);
});

unsubscribe();

offKeyed<K extends keyof T>(event: EventName<K>, key: Key, callback: (patches: Patches) => void): void

Unsubscribes a specific listener from a keyed event.

Parameters:

  • event - The event name in format ${fieldName}:updated
  • key - The specific key to unsubscribe from
  • callback - The exact callback function to remove

Example:

const callback = (patches) => console.log('Changed:', patches);
store.onKeyed('users:updated', 'user-123', callback);

store.offKeyed('users:updated', 'user-123', callback);

onceKeyed<K extends keyof T>(event: EventName<K>, key: Key, callback: (patches: Patches) => void): () => void

Subscribes to a keyed event for a single emission only.

Parameters:

  • event - The event name in format ${fieldName}:updated
  • key - The specific key to listen for
  • callback - Callback function that receives the patches array

Returns:

  • Unsubscribe function to remove listener before it fires

Example:

const unsubscribe = store.onceKeyed('users:updated', 'user-123', (patches) => {
  console.log('User 123 changed once:', patches);
});

// Callback will fire once, then automatically unsubscribe

onceKeyed<K extends keyof T>(event: EventName<K>, key: '*', callback: (key: ExtractKeyType<T[K]>, patches: Patches) => void): () => void

Subscribes to all keys within a field for a single emission only (wildcard).

Parameters:

  • event - The event name in format ${fieldName}:updated
  • key - Use '*' to listen to all keys
  • callback - Callback function that receives the key and patches array

Returns:

  • Unsubscribe function to remove listener before it fires

Example:

const unsubscribe = store.onceKeyed('users:updated', '*', (userId, patches) => {
  console.log(`User ${userId} changed once:`, patches);
});

// Callback will fire once, then automatically unsubscribe

getState(): T

Gets the entire current state.

Returns:

  • A shallow copy of the current state

Example:

const state = store.getState();
console.log(state);

subscriptions: SubscriptionsMap<T>

A convenient object with keys matching all state fields. Each field provides a subscribe function that:

  • Executes the callback immediately with the current field value
  • Executes the callback on every field change
  • Returns an unsubscribe function

Example:

// Subscribe to counter field
const unsubscribe = store.subscriptions.counter((counter) => {
  console.log('Counter value:', counter);
});

// Unsubscribe later
unsubscribe();

Type-safe access:

type State = {
  counter: number;
  user: { name: string };
};

const store = createObservableStore<State>({ ... });

// ✅ Valid - TypeScript knows these are the available fields
store.subscriptions.counter((counter) => { /* counter: number */ });
store.subscriptions.user((user) => { /* user: { name: string } */ });

// ❌ Type error - Invalid field name
store.subscriptions.invalid((value) => { /* Type error */ });

keyedSubscriptions: KeyedSubscriptionsMap<T>

A convenient object with keys matching all state fields. Each field provides a function that takes a key and returns a subscribe function. The subscribe function:

  • Executes the callback immediately with the current field value
  • Executes the callback on every field change for that specific key
  • Returns an unsubscribe function

Example:

// Subscribe to specific user updates
const unsubscribe = store.keyedSubscriptions.users('user-1')((users) => {
  console.log('User 1:', users['user-1'].name);
});

// Unsubscribe later
unsubscribe();

Type-safe access:

type State = {
  users: Record<string, { name: string }>;
  todos: Array<{ id: number; text: string }>;
};

const store = createObservableStore<State>({ ... });

// ✅ Valid - TypeScript knows these are the available fields
store.keyedSubscriptions.users('user-1')((users) => { /* users: Record<string, { name: string }> */ });
store.keyedSubscriptions.todos(0)((todos) => { /* todos: Array<{ id: number; text: string }> */ });

// ❌ Type error - Invalid field name
store.keyedSubscriptions.invalid('key')((value) => { /* Type error */ });

Type Safety

The library provides full TypeScript type safety:

type State = {
  user: { name: string };
  counter: { value: number };
};

const store = createObservableStore<State>({ ... });

// ✅ Valid
store.update((state) => {
  state.user.name = 'Jane';
});

// ❌ Type error: Invalid field name
store.update('invalid', (state) => {
  // Type error
});

// ✅ Valid
store.on('user:updated', (patches) => {
  console.log(patches);
});

// ❌ Type error: Invalid event name
store.on('invalid:updated', (patches) => {
  // Type error
});

// ❌ Type error: Primitive types not allowed
const invalidStore = createObservableStore<{
  count: number;
}>({ count: 0 });

// ✅ Keyed events with type safety
type State = {
  users: Record<string, { name: string }>;
};

const store = createObservableStore<State>({ users: {} });

// ✅ Valid
store.onKeyed('users:updated', 'user-1', (patches) => {
  console.log(patches);
});

// ❌ Type error: Invalid event name
store.onKeyed('invalid:updated', 'user-1', (patches) => {
  // Type error
});

// ✅ Subscribe API with type safety
type State = {
  counter: number;
  user: { name: string };
};

const store = createObservableStore<State>({ ... });

// ✅ Valid - TypeScript infers correct types
store.subscriptions.counter((counter) => {
  // counter is typed as number
  console.log(counter);
});

store.subscriptions.user((user) => {
  // user is typed as { name: string }
  console.log(user.name);
});

// ❌ Type error: Invalid field name
store.subscriptions.invalid((value) => {
  // Type error
});

Performance Considerations

Keyed events use conditional emission - they are only emitted when there are active keyed listeners. This means there's no performance overhead when you're not using keyed events:

type State = {
  users: Record<string, { name: string }>;
};

const store = createObservableStore<State>({ users: {} });

// No keyed listeners - no performance overhead
store.on('users:updated', (patches) => {
  console.log('Regular event:', patches);
});

store.update('users', (state) => {
  state['user-1'] = { name: 'John' };
});
// Only regular event is emitted

// Add keyed listener - now keyed events are emitted
store.onKeyed('users:updated', 'user-1', (patches) => {
  console.log('Keyed event:', patches);
});

store.update('users', (state) => {
  state['user-1'].name = 'Jane';
});
// Both regular and keyed events are emitted

Note: Numeric keys in Record objects are converted to strings by most patch generation libraries in patch paths, so subscribe using the string version of the key.

Working with Primitives

The top-level state must be a non-primitive object or array (enforced by TypeScript), but field values can be primitives when using patch-recorder (the default). For maximum compatibility across all patch generation libraries (including mutative/immer), wrap primitives in objects:

// ✅ Works with patch-recorder (default)
type State1 = {
  count: number;  // Primitive at field level works
  name: string;
  flag: boolean;
};

const store1 = createObservableStore<State1>({
  count: 0,
  name: 'John',
  flag: false
});

store1.update((state) => {
  state.count += 1;
});

// For maximum compatibility across all libraries (mutative, immer)
type State2 = {
  count: { value: number };  // Wrapped primitive
  name: { value: string };
  flag: { value: boolean };
};

// You can create a utility type for consistency
type PrimitiveField<T> = { value: T };

type BetterState = {
  count: PrimitiveField<number>;
  name: PrimitiveField<string>;
  flag: PrimitiveField<boolean>;
};

const store2 = createObservableStore<BetterState>({
  count: { value: 0 },
  name: { value: 'John' },
  flag: { value: false }
});

store2.update((state) => {
  state.count.value += 1;
});

Understanding Patches

Patches follow the JSON Patch (RFC 6902) format. The exact patch format depends on the patch generation library you use:

  • patch-recorder (default): Generates patches with minimal overhead while keeping references
  • mutative: Generates high-performance JSON patches with array optimizations
  • immer: Generates standard JSON patches
store.on('user:updated', (patches) => {
  patches.forEach(patch => {
    console.log(`Operation: ${patch.op}`);
    console.log(`Path: ${JSON.stringify(patch.path)}`);
    console.log(`Value: ${JSON.stringify(patch.value)}`);
  });
});

Common operations:

  • replace - Replace a value at a path
  • add - Add a value to an array or object
  • remove - Remove a value from an array or object

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.