Destam is a small state management library that:
- Lets you mutate state directly (no forced immutability / cloning)
- Tracks all changes as deltas (Insert / Modify / Delete)
- Gives you Observers that can:
- expose current state
- notify you when it changes (
.watch,.watchCommit,.effect, etc.) - narrow what you listen to with governors (
.path,.ignore,.shallow,.skip,.tree, …)
- Works with observable objects (
OObject) and observable arrays (OArray), which behave like normal JS objects/arrays but are fully tracked.
You can use those deltas to:
- Update a UI or DOM efficiently
- Sync state across tabs, clients, or to a server
- Implement undo/redo or time‑travel
- Sync to a DB or a document store
npm install destam
# or
yarn add destamAn Observer, the core concept behind destam, is simply a box around a value that:
- Stores a value
- Can be updated, if it’s mutable
- Notifies listeners
import { Observer } from 'destam';
const count = Observer.mutable(0);
console.log(count.get()); // 0
count.set(1);
console.log(count.get()); // 1.watch lets you subscribe to changes. The callback gets called every time the value is mutated.
const count = Observer.mutable(0);
const stop = count.watch(event => {
// For basic mutable observers:
// event.value = new value
// event.prev = previous value
console.log('count changed from', event.prev, 'to', event.value);
});
count.set(1); // logs: count changed from 0 to 1
count.set(2); // logs: count changed from 1 to 2
// stop listening
stop();A few things to know about .watch:
- It returns a cleanup function. Call it to unsubscribe.
- It runs synchronously right when the mutation happens.
- If multiple watchers are attached, they all get run (order is not guaranteed stable, and they can re‑trigger changes).
There are more Observer helpers (.map, .memo, .throttle, .wait, .unwrap, etc.), but for a quick start, mutable, get, set, and watch are enough. See the rest of the documentation for more details.
OObject is a JS object that:
- Behaves like a normal object
- Has a built‑in Observer through
.observerso you can use to watch mutations to any property - Emits Insert / Modify / Delete deltas when it changes
import { OObject } from 'destam';
const state = OObject({
name: 'John Doe',
address: 'Tokyo',
});You can read and write properties like a normal object:
console.log(state.name); // "John Doe"
state.name = 'Jane Doe'; // mutation is tracked
delete state.address; // also trackedEvery observable exposes .observer:
const obs = state.observer;
console.log(obs.get() === state); // trueFor observables like OObject and OArray, .watch receives delta objects:
Insert(prev, value, ref, id)Modify(prev, value, ref, id)Delete(prev, value, ref, id)
Where (for OObject):
event.ref– the property name (string)event.value– new valueevent.prev– previous valueevent.path– full path from the root observer (array of keys)event.parent– the observable that was mutated (here:state)
Example:
import { Insert, Modify, Delete } from 'destam';
state.observer.watch(event => {
if (event instanceof Insert) {
console.log('INSERT', event.path, '->', event.value);
} else if (event instanceof Modify) {
console.log('MODIFY', event.path, 'from', event.prev, 'to', event.value);
} else if (event instanceof Delete) {
console.log('DELETE', event.path, 'prev was', event.prev);
}
});
state.name = 'Jane Doe'; // MODIFY ["name"] from "John Doe" to "Jane Doe"
state.age = 42; // INSERT ["age"] -> 42
delete state.age; // DELETE ["age"] prev was 42Observers can be narrowed using governors. The most important one to start with: .path.
// Only react when "address" changes:
state.observer
.path('address')
.watch(event => {
console.log(`${event.parent.name}'s address changed to ${event.value}`);
});
state.address = 'Toronto';
// logs: "John Doe's address changed to Toronto"
state.occupation = 'Electrician';
// No log, because .watch was narrowed and only listens for updates
// to state.addressSome key points:
state.observersees everything understate.state.observer.path('address')only sees changes at/understate.address..pathworks for nested properties too (you can pass an array path).
OArray is a JS array that:
- Behaves like a normal array (
push,splice, indexing,Array.isArray,instanceof, etc.) - Emits deltas for element insertions, modifications, and deletions
- Uses stable logical indexes internally so you can track items even when the array shifts
import { OArray } from 'destam';
const arr = OArray([1, 2, 3]);
console.log(arr.length); // 3
console.log([...arr]); // [1, 2, 3]
arr.push(4); // tracked
arr[0] = 10; // tracked
arr.splice(1, 2, 'a', 'b'); // tracked (insert + delete/modify)It still behaves like an array:
console.log(Array.isArray(arr)); // true
console.log(arr instanceof OArray); // trueNote:
sortandreverseare intentionally disabled (they throw) because they’re not implemented for stable indexing.
The .observer for an OArray also gives you Insert, Modify, Delete deltas. The difference vs. OObject is the ref: in arrays it’s a stable index token instead of a numeric JS index.
Example:
import { Insert } from 'destam';
const arr = OArray([1, 2, 3]);
arr.observer.watch(event => {
if (event instanceof Insert) {
console.log('Inserted value', event.value, 'at ref', event.ref);
} else {
console.log(event.constructor.name, 'prev=', event.prev, 'value=', event.value);
}
});
arr.push(4);
// Insert at logical end
arr[0] = 10;
// Modify of first element
arr.splice(1, 2);
// Deletes / modifies depending on what changedYou rarely need to care about the exact internal index encoding. There are helpers if you need to map between numeric positions and refs.
From Array.js Destam exports:
indexPosition(array, ref)– given a ref from an event, returns the numeric position at that moment.positionIndex(array, pos)– given a numeric position, returns the stable ref for that element (useful in.pathgovernors).
Example: tracking which index changed in a watcher:
import { indexPosition } from 'destam/Array.js';
const arr = OArray();
arr.observer.watch(event => {
const idx = indexPosition(arr, event.path[0]); // path[0] is the ref
console.log('Change at index', idx, '->', event.value);
});
arr.push('a'); // logs: Change at index 0 -> a
arr.push('b'); // logs: Change at index 1 -> b
arr.push('c'); // logs: Change at index 2 -> cTo watch a specific element stably (even if the array shifts), you convert the numeric index to a ref using positionIndex and then use .path with that ref:
import { positionIndex } from 'destam/Array.js';
const arr = OArray(['hello', 'third thing']);
arr.splice(1, 0, 'world'); // arr = ["hello", "world", "third thing"]
// Grab stable refs for each position:
const ref0 = positionIndex(arr, 0);
const ref1 = positionIndex(arr, 1);
const ref2 = positionIndex(arr, 2);
// Create observers for each position:
const o0 = arr.observer.path([ref0]);
const o1 = arr.observer.path([ref1]);
const o2 = arr.observer.path([ref2]);
console.log(o0.get(), o1.get(), o2.get()); // "hello", "world", "third thing"
// Update via observers:
o0.set('new value 1');
o1.set('new value 2');
o2.set('new value 3');
// These are equivalent to writing directly: arr[0] = ..., etc.
console.log([...arr]); // ["new value 1", "new value 2", "new value 3"]Under the hood, these .path([ref]) observers:
- Resolve the ref into a numeric index at call time
- Read/write the proper element
- Emit the
Modifydeltas
You’ll see .watch in three main situations:
-
Plain Observer (single value)
- Created with
Observer.mutable(),.map,.all, etc. watch(cb)callscb(event)whenever.setis called.event.valueandevent.prevare the new/previous values.
- Created with
-
Observable Object (
OObject)state.observer.watch(cb)gets per‑property events:Insert,Modify, orDeleteevent.refis the property nameevent.pathis path from root observer (array of keys)
-
Observable Array (
OArray)arr.observer.watch(cb)gets per‑element events:Insert,Modify, orDeleteevent.refis a stable index ref- Use
indexPosition(arr, event.ref)to get the numeric index.
In all three:
.watchreturns a function to unsubscribe.- Multiple listeners can be attached.
- Events are emitted synchronously on mutation.
If you want to dig deeper:
- Observers & governors:
docs/observer.md,docs/governors.md - Observables & state trees:
docs/observables.md,docs/state-tree.md - Networks, commits, undo/redo & syncing:
docs/network.md,Tracking.js
But for most use cases, you can start with:
Observer.mutable+.get(),.set(),.watch()OObject+.observer.watch()/.observer.path(...)OArray+.observer.watch(),indexPosition,positionIndex
and only pull in the more advanced stuff when you actually need it.
./
├── README.md <- this readme
├── destam <- The core destam library (Observer, OArray, OObject, etc)
│ └── ...
└── destam-react <- Library containing specialized Observer integration tools for React
└── ...Documentation: