Created
January 26, 2023 20:49
-
-
Save BrianHung/6c384940c1ad18c2fbe4be8773a70a60 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | |
} | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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