perf(history): batch transform drag undo

This commit is contained in:
syntaxbullet
2026-07-04 15:49:13 +02:00
parent a030f2065a
commit 1acfcbe4a5
5 changed files with 159 additions and 8 deletions

View File

@@ -1,6 +1,5 @@
import type { AppState, HistorySnapshot } from "@editor/state";
import type { CommandContext } from "./command";
import { commandIds } from "./ids";
import type { CommandContext, CommandHistoryPolicy } from "./command";
import type { CommandId, CommandPayloads } from "./payloads";
import type { CommandRegistry } from "./registry";
@@ -15,6 +14,8 @@ export function createCommandDispatcher(options: {
getState: () => AppState;
setState: (state: AppState) => void;
}): CommandDispatcher {
let deferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined;
return {
dispatch(commandId, payload) {
const command = options.registry.get(commandId);
@@ -23,28 +24,89 @@ export function createCommandDispatcher(options: {
}
const currentState = options.getState();
const historyPolicy = command.history ?? defaultHistoryPolicy;
const deferredSnapshot = historyPolicy.mode === "deferred" && historyPolicy.phase === "begin" ? snapshot(currentState) : undefined;
const context: CommandContext = { state: currentState };
const executedState = command.execute(context, payload);
if (executedState === currentState) {
if (historyPolicy.mode === "deferred" && historyPolicy.phase === "commit") deferredHistory = undefined;
return currentState;
}
const nextState = shouldRecordHistory(commandId, currentState, executedState) ? recordHistory(currentState, executedState) : executedState;
const nextState = applyHistoryPolicy({
currentState,
nextState: executedState,
historyPolicy,
deferredSnapshot,
getDeferredHistory: () => deferredHistory,
setDeferredHistory: (nextDeferredHistory) => {
deferredHistory = nextDeferredHistory;
},
});
options.setState(nextState);
return nextState;
},
};
}
function shouldRecordHistory(commandId: CommandId, currentState: AppState, nextState: AppState) {
if (commandId === commandIds.historyUndo || commandId === commandIds.historyRedo) return false;
const defaultHistoryPolicy: CommandHistoryPolicy = { mode: "auto" };
function applyHistoryPolicy(options: {
currentState: AppState;
nextState: AppState;
historyPolicy: CommandHistoryPolicy;
deferredSnapshot?: HistorySnapshot;
getDeferredHistory: () => { snapshot: HistorySnapshot; changed: boolean } | undefined;
setDeferredHistory: (nextDeferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined) => void;
}): AppState {
if (options.historyPolicy.mode === "ignore") {
options.setDeferredHistory(undefined);
return options.nextState;
}
if (options.historyPolicy.mode === "deferred") {
return applyDeferredHistoryPolicy(options);
}
return shouldRecordHistory(options.currentState, options.nextState) ? recordHistory(snapshot(options.currentState), options.nextState) : options.nextState;
}
function applyDeferredHistoryPolicy(options: {
currentState: AppState;
nextState: AppState;
historyPolicy: Extract<CommandHistoryPolicy, { mode: "deferred" }>;
deferredSnapshot?: HistorySnapshot;
getDeferredHistory: () => { snapshot: HistorySnapshot; changed: boolean } | undefined;
setDeferredHistory: (nextDeferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined) => void;
}): AppState {
switch (options.historyPolicy.phase) {
case "begin":
options.setDeferredHistory(options.deferredSnapshot ? { snapshot: options.deferredSnapshot, changed: false } : undefined);
return options.nextState;
case "update": {
const deferredHistory = options.getDeferredHistory();
if (deferredHistory && options.currentState.document !== options.nextState.document) {
options.setDeferredHistory({ ...deferredHistory, changed: true });
}
return options.nextState;
}
case "commit": {
const deferredHistory = options.getDeferredHistory();
options.setDeferredHistory(undefined);
return deferredHistory?.changed ? recordHistory(deferredHistory.snapshot, options.nextState) : options.nextState;
}
}
}
function shouldRecordHistory(currentState: AppState, nextState: AppState) {
return currentState.document !== nextState.document;
}
function recordHistory(currentState: AppState, nextState: AppState): AppState {
function recordHistory(historySnapshot: HistorySnapshot, nextState: AppState): AppState {
return {
...nextState,
history: {
past: [...currentState.history.past, snapshot(currentState)].slice(-100),
past: [...nextState.history.past, historySnapshot].slice(-100),
future: [],
},
};