Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
// Browser sync program
import { uniqueId } from "lodash";
import React, { useState } from "react";
import ReactDOM from "react-dom";
type Cursor = number[];
type DeserializedMutationRecord =
| {
type: "childList";
target: Node;
addedNodes: Node[];
removedNodes: Node[];
previousSibling: Node | void;
nextSibling: Node | void;
}
| {
type: "characterData";
target: Node;
value: string;
}
| {
type: "attributes";
target: Node;
attributeName: string;
oldValue: string;
value: string;
};
type SerializedMutationRecord =
| {
type: "childList";
targetCursor: Cursor;
removedNodesCursors: Cursor[];
addedNodesData: {
tag: string;
attributes: object;
html: string;
}[];
previousSiblingCursor: Cursor | void;
nextSiblingCursor: Cursor | void;
}
| {
type: "characterData";
targetCursor: Cursor;
value: string;
}
| {
type: "attributes";
targetCursor: Cursor;
attributeName: string;
oldValue: string;
value: string;
};
function getNodeByCursor(cursor: Cursor, root: Node): Node {
let cur: Node = root;
for (const idx of cursor) {
cur = cur.childNodes[idx];
}
return cur;
}
function getCursorFromRoot(target: Node, root: Node): Cursor {
if (target === root) {
return [];
}
// if (!isChildOfRoot(target, root)) {
// throw new Error('Is not child of root')
// }
if (target == null) {
throw new Error("not valid root");
}
if (target.parentElement === root) {
return [0];
}
const indexFromParent = Array.from(target.parentElement.childNodes).indexOf(
target as ChildNode
);
return [...getCursorFromRoot(target.parentElement, root), indexFromParent];
}
function deserializeMutationRecord(
record: SerializedMutationRecord,
host: HTMLElement
): DeserializedMutationRecord {
switch (record.type) {
case "characterData": {
return {
type: record.type,
target: getNodeByCursor(record.targetCursor, host),
value: record.value
};
}
case "attributes": {
return {
type: record.type,
target: getNodeByCursor(record.targetCursor, host) as any,
attributeName: record.attributeName,
oldValue: record.oldValue,
value: record.value
};
}
case "childList": {
return {
type: record.type,
addedNodes: record.addedNodesData.map(data => {
const el = document.createElement(data.tag);
Object.entries(data.attributes).forEach(([k, v]) =>
el.setAttribute(k, v)
);
el.innerHTML = data.html;
return el;
}),
removedNodes: record.removedNodesCursors.map(n =>
getNodeByCursor(n, host)
),
target: getNodeByCursor(record.targetCursor, host),
previousSibling:
record.previousSiblingCursor &&
getNodeByCursor(record.previousSiblingCursor, host),
nextSibling:
record.nextSiblingCursor &&
getNodeByCursor(record.nextSiblingCursor, host)
};
}
}
}
function serializeMutationRecord(
record: MutationRecord,
host: HTMLElement,
cursorMap: Map<Node, Cursor>
): SerializedMutationRecord {
switch (record.type) {
case "characterData": {
const text = record.target as Text;
// console.log("char", record.target.data);
return {
type: record.type,
targetCursor: getCursorFromRoot(record.target, host),
value: text.data
};
}
case "attributes": {
return {
type: record.type,
targetCursor: getCursorFromRoot(record.target, host),
attributeName: record.attributeName,
oldValue: record.oldValue,
value: host.getAttribute(record.attributeName)
};
}
case "childList": {
return {
type: record.type,
targetCursor: getCursorFromRoot(record.target, host),
removedNodesCursors:
record.removedNodes &&
Array.from(record.removedNodes).map(removedNode =>
cursorMap.get(removedNode)
),
addedNodesData:
record.addedNodes &&
Array.from(record.addedNodes).map((addedNode: Element) => ({
tag: addedNode.tagName.toLowerCase(),
attributes: Object.entries(addedNode.attributes).reduce(
(acc, [k, v]) => ({ ...acc, [k]: v }),
{}
),
html: addedNode.innerHTML
})),
previousSiblingCursor: cursorMap.get(record.previousSibling),
nextSiblingCursor: cursorMap.get(record.nextSibling)
};
}
}
}
function isChildOfRoot(node: Node, root: Node) {
if (node === root) {
return true;
}
let cur = node;
while ((cur = cur.parentElement)) {
if (cur === document.body) {
return false;
}
if (cur === root) {
return true;
}
}
return false;
}
function handleMutation(mut: DeserializedMutationRecord, root: HTMLElement) {
if (isChildOfRoot(mut.target, root)) {
switch (mut.type) {
case "attributes": {
const targetOfOutput = mut.target;
const newValue = (mut.target as Element).getAttribute(
mut.attributeName
);
(targetOfOutput as Element).setAttribute(mut.attributeName, newValue);
break;
}
case "characterData": {
// const mapped = this.getMappedNode(mut.target);
// const mapped = mut.target;
if (mut.target) {
mut.target.nodeValue = mut.value;
}
break;
}
case "childList": {
// added
if (mut.addedNodes.length > 0) {
for (const addedNode of Array.from(mut.addedNodes)) {
const newNode = addedNode.cloneNode(true);
if (mut.nextSibling) {
mut.target.insertBefore(newNode, mut.nextSibling);
} else if (mut.previousSibling) {
if (mut.previousSibling) {
mut.target.insertBefore(
newNode,
mut.previousSibling.nextSibling
);
}
mut.target.appendChild(newNode);
} else {
mut.target.appendChild(newNode);
}
}
}
// removed
if (mut.removedNodes.length > 0) {
for (const removedNode of Array.from(mut.removedNodes)) {
const removedOnOut = removedNode;
const targetOnOutput = mut.target;
targetOnOutput.removeChild(removedOnOut);
}
}
}
}
}
}
class SyncSource {
private lastCursorMap: Map<Node, Cursor> = new Map();
constructor(private root: HTMLElement) {}
startSync(onUpdate: (serialized: SerializedMutationRecord[]) => void) {
new MutationObserver((mutations: MutationRecord[]) => {
const serialized = Array.from(mutations)
.filter(mut => {
return isChildOfRoot(mut.target, this.root);
})
.map(mut =>
serializeMutationRecord(mut, this.root, this.lastCursorMap)
);
this.rebuildCursorMap();
onUpdate(JSON.parse(JSON.stringify(serialized)));
}).observe(document.body, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
}
private rebuildCursorMap() {
this.lastCursorMap.clear();
const walk = (node: Node) => {
const cursor = getCursorFromRoot(node, this.root);
this.lastCursorMap.set(node, cursor);
Array.from(node.childNodes).forEach(child => walk(child));
};
walk(this.root);
}
}
function sync(serialized: SerializedMutationRecord[], root: HTMLElement) {
// run
const deserialized: DeserializedMutationRecord[] = serialized.map(
(mut: SerializedMutationRecord) => deserializeMutationRecord(mut, root)
);
for (const mut of Array.from(deserialized)) {
handleMutation(mut, root);
}
}
// React Component
function NodeList(props: { value: number; onClickSelf: () => void }) {
const [children, setChildren] = useState([]);
const [counter, setCounter] = useState(0);
return (
<div>
<button onClick={() => setCounter(s => s + 1)}>{counter}</button>
<button
onClick={() => {
setChildren([...children, uniqueId()]);
}}
>
add
</button>
<button
onClick={() => {
setChildren([]);
}}
>
clear
</button>
<button onClick={props.onClickSelf}>remove self</button>
<span>{props.value}</span>
<ul>
{children.map((i, index) => {
return (
<NodeList
key={i}
value={i}
onClickSelf={() => {
const newChildren = children.filter(
(_, cidx) => cidx !== index
);
setChildren(newChildren);
}}
/>
);
})}
</ul>
</div>
);
}
function App() {
return <NodeList value={0} onClickSelf={() => {}} />;
}
// run
const observedNode = document.querySelector(".region-a") as HTMLElement;
const outputNode = document.querySelector(".region-b") as HTMLElement;
const source = new SyncSource(observedNode);
source.startSync(serialized => {
sync(serialized, outputNode);
});
ReactDOM.render(<App />, observedNode);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment