A simple wrapper around Automerge documents, with Redux Dev Tools integration and experimental undo/redo support. It works with both vanilla Automerge docs and DocHandle
s from Automerge Repo.
npm install @onsetsoftware/automerge-store
import { AutomergeStore } from "@onsetsoftware/automerge-store";
import { from } from "@automerge/automerge";
type DocState = {
count: number;
};
const doc = from({
count: 0,
});
const docId = "documentId";
const store = new AutomergeStore(docId, doc);
import { AutomergeRepoStore } from "@onsetsoftware/automerge-store";
import { DocHandle, DocumentId, Repo } from "@automerge/automerge-repo";
import { LocalForageStorageAdapter } from "@automerge/automerge-repo-storage-localforage";
import * as localforage from "localforage";
type DocState = {
count: number;
};
const repo = new Repo({
network: [],
storage: new LocalForageStorageAdapter(),
});
const initialState: DocState = {
count: 0,
};
// load a document from the repo, or create a new one
localforage.getItem("rootDocId").then(async (docId) => {
let handle: DocHandle<DocState>;
if (!docId) {
// the doc doesn't exist, so create it, save the ID and then initialise it
handle = repo.create<DocState>();
localforage.setItem("rootDocId", handle.documentId);
handle.change((doc) => {
Object.assign(doc, initialState);
});
} else {
handle = repo.find(docId as DocumentId);
}
const store = new AutomergeRepoStore(handle);
});
await store.ready();
const unsubscribe = store.subscribe((doc) => {
// update your UI/update another state store
console.log(doc);
});
// unsubscribe
unsubscribe();
// wait for the store to be ready before making changes
await store.ready();
store.change((doc) => {
doc.count += 1;
});
Implementing Undo/Redo is not trivial, especially when it comes to handling network sync too. Currently undo patches are generated within a requestIdleCallback
wrapper so that generating them does not block the renderer.
Undo/Redo is supported by the undo
and redo
methods:
store.undo();
store.redo();
You can also check if there are any undo/redo actions available:
store.canUndo();
store.canRedo();
If you need to group a number of changes from different parts of your application into a single undo/redo action, you can use transactions:
store.transaction(() => {
store.change((doc) => {
doc.count += 1;
});
store.change((doc) => {
doc.count += 1;
});
store.change((doc) => {
doc.count += 1;
});
});
console.log(store.doc.count); // 3
// undoing the transaction will undo all of the changes
store.undo(); // doc.count === 0
You can also pass a message to the transaction, which will be passed to automerge as the change message and displayed in Redux Dev Tools. Either return a string from the transaction function, or pass it as the second argument:
store.transaction(
() => {
store.change((doc) => {
doc.count += 1;
});
store.change((doc) => {
doc.count += 1;
});
store.change((doc) => {
doc.count += 1;
});
return "Incremented count";
},
// alternatively you can pass the message as the second argument. The return value will be used if both are provided
"Incremented count"
);
Opt in to Redux Dev Tools support by passing the devTools
option to the constructor:
const store = new AutomergeStore(docId, doc, {
withDevTools: true,
});
For DevTools to work, the Redux Dev Tools browser extension needs to be installed and enabled. Time travel is fully functional, but persisting the state across page reloads is not yet supported.