import { applySnapshot, getEnv, getSnapshot, onSnapshot, resolvePath, types } from "mobx-state-tree";
|
import { FF_DEV_1284, isFF } from "../utils/feature-flags";
|
|
/**
|
* Time Traveller
|
*/
|
const TimeTraveller = types
|
.model("TimeTraveller", {
|
undoIdx: 0,
|
targetPath: "",
|
skipNextUndoState: types.optional(types.boolean, false),
|
lastAdditionTime: types.optional(types.Date, new Date()),
|
createdIdx: 0,
|
})
|
.volatile(() => ({
|
history: [],
|
isFrozen: false,
|
}))
|
.views((self) => ({
|
get canUndo() {
|
return self.undoIdx > 0;
|
},
|
get canRedo() {
|
return self.undoIdx < self.history.length - 1;
|
},
|
get hasChanges() {
|
return self.history.length > 1;
|
},
|
}))
|
.actions((self) => {
|
let targetStore;
|
let snapshotDisposer;
|
const updateHandlers = new Set();
|
// A way to handle multiple simultaneous freezes from different places
|
const freezingLockSet = new Set();
|
let changesDuringFreeze = false;
|
let replaceNextUndoState = false;
|
|
function triggerHandlers(force = true) {
|
updateHandlers.forEach((handler) => handler(force));
|
}
|
|
return {
|
freeze(key) {
|
freezingLockSet.add(key);
|
if (!self.isFrozen) {
|
changesDuringFreeze = false;
|
self.isFrozen = true;
|
}
|
},
|
|
safeUnfreeze(key) {
|
freezingLockSet.delete(key);
|
self.isFrozen = freezingLockSet.size > 0;
|
},
|
|
unfreeze(key) {
|
self.safeUnfreeze(key);
|
if (!self.isFrozen) {
|
if (changesDuringFreeze) self.recordNow();
|
self.setReplaceNextUndoState(false);
|
}
|
},
|
|
setSkipNextUndoState(value = true) {
|
self.skipNextUndoState = value;
|
},
|
|
setReplaceNextUndoState(value = true) {
|
replaceNextUndoState = value;
|
},
|
|
recordNow() {
|
if (!targetStore) return;
|
|
self.addUndoState(getSnapshot(targetStore));
|
},
|
|
onUpdate(handler) {
|
updateHandlers.add(handler);
|
return () => {
|
updateHandlers.delete(handler);
|
};
|
},
|
|
addUndoState(recorder) {
|
if (self.isFrozen) {
|
changesDuringFreeze = true;
|
return;
|
}
|
if (self.skipNextUndoState) {
|
/**
|
* Skip recording if this state was caused by undo / redo
|
*/
|
self.skipNextUndoState = false;
|
|
return;
|
}
|
|
// mutate history to trigger history-related UI items
|
self.history = self.history.slice(0, self.undoIdx + !replaceNextUndoState).concat(recorder);
|
self.undoIdx = self.history.length - 1;
|
replaceNextUndoState = false;
|
changesDuringFreeze = false;
|
self.lastAdditionTime = new Date();
|
},
|
|
reinit(force = true) {
|
self.history = [getSnapshot(targetStore)];
|
self.undoIdx = 0;
|
self.createdIdx = 0;
|
triggerHandlers(force);
|
},
|
|
afterCreate() {
|
targetStore = self.targetPath ? resolvePath(self, self.targetPath) : getEnv(self).targetStore;
|
|
if (!targetStore)
|
throw new Error(
|
"Failed to find target store for TimeTraveller. Please provide `targetPath` property, or a `targetStore` in the environment",
|
);
|
// start listening to changes
|
snapshotDisposer = onSnapshot(targetStore, (snapshot) => this.addUndoState(snapshot));
|
// record an initial state if no known
|
if (self.history.length === 0) {
|
self.recordNow();
|
}
|
|
self.createdIdx = self.undoIdx;
|
},
|
|
beforeDestroy() {
|
snapshotDisposer();
|
targetStore = null;
|
snapshotDisposer = null;
|
updateHandlers.clear();
|
freezingLockSet.clear();
|
},
|
|
undo() {
|
self.set(self.undoIdx - 1);
|
},
|
|
redo() {
|
self.set(self.undoIdx + 1);
|
},
|
|
set(idx) {
|
self.undoIdx = idx;
|
self.skipNextUndoState = true;
|
applySnapshot(targetStore, self.history[idx]);
|
triggerHandlers();
|
if (isFF(FF_DEV_1284)) {
|
setTimeout(() => {
|
// Prevent skiping next undo state if onSnapshot event was somehow missed after applying snapshot
|
self.setSkipNextUndoState(false);
|
});
|
}
|
},
|
|
reset() {
|
// just apply zero state; it would be added as a new hisory item
|
applySnapshot(targetStore, self.history[self.createdIdx]);
|
triggerHandlers();
|
},
|
};
|
});
|
|
export default TimeTraveller;
|