Skip to content

Instantly share code, notes, and snippets.

@BrianHung
Created January 26, 2023 20:49
Show Gist options
  • Save BrianHung/6c384940c1ad18c2fbe4be8773a70a60 to your computer and use it in GitHub Desktop.
Save BrianHung/6c384940c1ad18c2fbe4be8773a70a60 to your computer and use it in GitHub Desktop.
export function debounce<T extends (...args: any[]) => void>({
callback,
onStart,
onEnd,
delay = 100,
}: {
callback: T;
onStart?: T;
onEnd?: T;
delay?: number;
}): T {
let timer: number | undefined;
const debounced = function (this: any, ...args: any[]) {
callback.apply(this, args);
if (!timer) onStart?.apply(this, args);
else window.clearTimeout(timer);
timer = window.setTimeout(() => {
onEnd?.apply(this, args);
timer = undefined;
}, delay);
};
return debounced;
}
import { IJsonPatch, onPatch, applyPatch, recordPatches } from "mobx-state-tree";
import { EditorView } from "../../EditorView";
import { MSTEditorState } from "../editor";
import { MSTCommand } from "../mutators";
import { groupPatchesByKey } from "./patches";
// Parameterized command.
export type pCommand = (args: any) => MSTCommand;
export function dispatch(command: any, view: EditorView, undoable: boolean = true) {
return async function (args: Parameters<T>[0]) {
const recorder = recordPatches(view.state);
command(args)({ state: view.state });
recorder.stop();
const { patches, reversedInversePatches: reverse } = recorder;
const grouped = groupPatchesByKey(patches);
view.replicache?.mutate.writePatches(grouped);
if (undoable) {
await view.undoManager.add({
redo() {
const grouped = groupPatchesByKey(patches);
view.replicache?.mutate.writePatches(grouped);
},
undo() {
const grouped = groupPatchesByKey(reverse);
view.replicache?.mutate.writePatches(grouped);
},
});
}
};
}
export function testDispatch(command: any, state: MSTEditorState) {
return function (args: Parameters<T>[0]) {
const recorder = recordPatches(state);
command(args)({ state });
recorder.stop();
const { patches } = recorder;
return groupPatchesByKey(patches);
};
}
export function localDispatch(command: any, view: EditorView, undoable: boolean = true) {
return async function (args: Parameters<T>[0]) {
const recorder = recordPatches(view.state);
command(args)({ state: view.state });
recorder.stop();
if (undoable) {
const { replay: redo, undo } = recorder;
await view.undoManager.add({ redo, undo });
}
};
}
const onRowCountChange = useCallback(
debounce({
callback(count: number) {
dispatch(setRowCountAndFitGrid, view)({ id: node.id, count });
},
onStart() {
view.undoManager.endGroup();
view.undoManager.startGroup();
},
onEnd() {
view.undoManager.endGroup();
},
delay: 200,
}),
[node]
);
import { type ExperimentalDiffOperation, type ReadonlyJSONValue, type WriteTransaction } from "@rocicorp/reflect";
import { applyPatches, enablePatches, Patch } from "immer";
import { joinJsonPath, splitJsonPath, type IJsonPatch } from "mobx-state-tree";
import invariant from "tiny-invariant";
import { IEditorState } from "../editor";
enablePatches();
const applyPatchesToKey = async (tx: WriteTransaction, { key, patches }) => {
// This handles nested updates where you never see the entire parent
if (Array.isArray(patches)) {
let parent = await tx.get(key);
invariant(
typeof parent === "object" && parent != undefined,
`Can't update non-object ${parent} at key ${key} with patches:\n${JSON.stringify(patches, null, 2)}}`
);
parent = applyPatches(parent, patches);
return tx.put(key, parent);
}
const { op, value } = patches;
return op !== "remove" ? tx.put(key, value) : tx.del(key);
};
// groupPatchesByKey before to avoid await tx.put between writes.
// TODO: Rename to applyPatchesToKeys on redeploy.
export const writePatches = (tx: WriteTransaction, patches: any) =>
Promise.all(patches.map(p => applyPatchesToKey(tx, p)));
export const diffToPatch = (diff: ExperimentalDiffOperation<string>): IJsonPatch => {
let { op, newValue: value, key: path } = diff;
if (!path.startsWith("/")) path = `/${path}`;
if (op === "add") return { op: "add", path, value };
if (op === "change") return { op: "replace", path, value };
if (op === "del") return { op: "remove", path };
throw Error(`Invalid diff:\n${JSON.stringify(diff, null, 2)}`);
};
export const entryToPatch = ([key, value]: readonly [key: string, value: ReadonlyJSONValue]): IJsonPatch => ({
path: key.startsWith("/") ? key : `/${key}`,
value,
op: "add" as const,
});
export const getImmerPatch = ({ path, ...patch }: IJsonPatch) => ({ path: splitJsonPath(path), ...patch });
export const getMSTPatch = ({ path, ...patch }: Patch) => ({ path: joinJsonPath(path as string[]), ...patch });
// Utility method to mock tx.get.
function getValueByPath(obj, path: string) {
let val = obj;
const keys = splitJsonPath(path);
for (const k of keys) {
if (val[k] === undefined) return undefined;
val = val[k];
}
return val;
}
// Compress patches from onPatch using snapshot of end state.
export function compressPatches(state: IEditorState, patches: IJsonPatch[]) {
const patchMap = new Map();
for (const { op, path } of patches) {
const key = path.split("/").slice(0, 3).join("/");
// Only delete value when entire object at key is removed.
if (key === path && op === "remove") patchMap.set(key, undefined);
else patchMap.set(key, getValueByPath(state, key));
}
return Array.from(patchMap).map(([path, value]) => ({ path, value, op: value ? "add" : "remove" }));
}
// Compute replicache key for a patch.
const getKeyPatch = ({ path, paths = splitJsonPath(path), ...patch }) => ({
path: paths.slice(2),
key: joinJsonPath(paths.slice(0, 2)),
...patch,
});
const getKeyMap = (map, { key, ...patch }) => map.set(key, (map.get(key) || []).concat(patch));
// Shallowly compresses patches by finding parent value if it exists
// and applying child patches to it.
const shallowCompress = (patches: Patch[]) => {
for (let i = patches.length - 1; i >= 0; i--) {
let { op, value: parent, path } = patches[i];
if (path.length === 0) {
parent = applyPatches(parent, patches.slice(i + 1));
return { op, path, value: parent };
}
}
return patches;
};
const compress = ([key, patches]) => ({ key, patches: shallowCompress(patches) });
const identity = ([key, patches]) => ({ key, patches });
export const groupPatchesByKey = (
patches: readonly IJsonPatch[]
): { key: string; patches: IJsonPatch | IJsonPatch[] }[] => {
const keyPatches = patches.map(getKeyPatch);
// Use ES6 Map to group patches by keys as it keeps the insertion order.
const keyMap = keyPatches.reduce(getKeyMap, new Map());
return Array.from(keyMap, compress);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment