Track client-side transactional changes to data — with undo/redo, edge dedup, superimpose, and batched or per-item saves. Fully typed, immutable, zero runtime dependencies.
npm i transactor-tstransactor-ts records an ordered sequence of local changes ("transactions")
against sets of data, so you can operate on them individually or as a whole. Build
up a batch of edits in the UI, undo/redo through them, superimpose them onto data
you already have, then flush them to your backend in one or many calls. Each
instance is isolated, so you can track several independent sets of changes at once.
It was purpose-built for the case where a server mutates data on save and the client needs to mimic that state locally before saving — hence the distinction between saveable and non-saveable transactions.
import { create } from 'transactor-ts';
interface User {
id: number;
name: string;
}
const t = create<User>();
t.add(1, { id: 1, name: 'Ada' }); // update (the default)
t.add(1, { id: 1, name: 'Ada L.' }); // another change to the same record
t.add(2, { id: 2, name: 'Alan' }, { add: true });
// All transactions, in order:
t.get();
// Latest transaction per id (edge dedup):
t.getLatestEdge();
// Undo up to and including the last saveable transaction:
t.back();
// Redo it:
t.forward();
// Flush to a backend — one call per operation type:
await t.save(
(updates) => api.put(updates), // put — updates
(creates) => api.post(creates), // post — adds
(deletes) => api.del(deletes), // del — deletes
);The first argument to add(id, data) groups transactions. These three are seen as
one record with three transactions:
t.add(1, { id: 1, value: 'a' });
t.add(1, { id: 2, value: 'b' });
t.add(1, { id: 3, value: 'c' });while these are seen as three records with one transaction each:
t.add(1, { id: 1, value: 'a' });
t.add(2, { id: 1, value: 'b' });
t.add(3, { id: 1, value: 'c' });Apply the latest-edge transactions onto a copy of data you already hold. The input is never mutated.
const clientData = [{ id: 1, val: 'test' }];
t.add(1, { id: 1, val: 'updated' });
t.superimpose(clientData.map((cd) => ({ id: cd.id, data: cd })));
// => [{ id: 1, data: { id: 1, val: 'updated' } }]A transaction added with { delete: true } removes the matching record from the
result.
| Method | Description |
|---|---|
create(options?) |
Factory returning a Transactor instance. |
new Transactor(options?) |
Same as create; the class is exported too. |
init(get?, set?) |
Inject custom storage get/set. No args resets to a fresh in-memory store. |
add(id, data, options?) |
Add a transaction. Clears the redo stack. |
asyncAdd(id, data, options?) |
Add asynchronously, preserving submission order across concurrent calls. Returns a Promise. |
get() |
All transactions { id, data, options } in order. |
getLatestEdge() |
Latest transaction per unique id (edge dedup). |
back() |
Undo up to and including the last saveable transaction. |
forward() |
Redo up to and including the last undone saveable transaction. |
superimpose(clientData) |
Apply latest-edge transactions onto a copy of clientData. Never mutates the input. |
save(put?, post?, del?) |
Batch: sort saveable transactions into add/update/delete arrays; call each handler once, if any. |
saveLatestEdge(put?, post?, del?) |
As save, over the latest-edge transactions. |
saveEach(put?, post?, del?) |
Call the matching handler once per saveable transaction, in order. |
saveEachEdge(put?, post?, del?) |
As saveEach, over the latest-edge transactions. |
clear() |
Remove all transactions for this instance and clear the redo stack. |
destroy() |
Remove this instance's data from the backing store. |
add(id, data, options) accepts:
| Option | Default | Meaning |
|---|---|---|
add |
— | Treat as a create; routed to the post handler on save. |
update |
true (when no type is given) |
Treat as an update; routed to the put handler. |
delete |
— | Treat as a delete; routed to del, and drives superimpose. |
save |
true (unless set to false) |
Whether the transaction is sent to handlers and forms undo/redo boundaries. |
All save handlers may return a promise; every save method returns a Promise that
resolves once all handler promises resolve. If a transaction needs a handler that
was not supplied, a clear error is thrown.
back()/forward()move up to and including the last saveable transaction, so a saveable change plus any trailing non-saveable changes are undone/redone as a unit.- Adding a new transaction clears the redo stack.
- Edge dedup (
getLatestEdge,saveLatestEdge,saveEachEdge,superimpose) keeps the latest transaction per id, with two nuances: add-then-update stays anadd, and add-then-delete cancels out entirely.
transactor-ts is the first-class TypeScript successor to the original
sequence-transactor (last
published as 2.1.2). The public API is unchanged — the same
create/init/Transactor, the same methods and option semantics — now with
generics over your data type, bundled type declarations, ESM output, strict typing,
and full test coverage. To migrate, install transactor-ts and update the import
specifier; no code changes are required.
Behavioural fix in this rewrite: superimpose no longer corrupts data when a
delete transaction targets an id that is not present in the client data (the
original removed the wrong element). Internals are now immutable — stored arrays and
caller-provided arrays/objects are never mutated.
BSD-3-Clause © 2018–2026 Daniel Cassil