@tldraw/sync-core
Version:
tldraw infinite canvas SDK (multiplayer sync).
197 lines (196 loc) • 6.07 kB
JavaScript
import { isEqual, objectMapEntries, objectMapValues } from "@tldraw/utils";
const RecordOpType = {
Put: "put",
Patch: "patch",
Remove: "remove"
};
function getNetworkDiff(diff) {
let res = null;
for (const [k, v] of objectMapEntries(diff.added)) {
if (!res) res = {};
res[k] = [RecordOpType.Put, v];
}
for (const [from, to] of objectMapValues(diff.updated)) {
const diff2 = diffRecord(from, to);
if (diff2) {
if (!res) res = {};
res[to.id] = [RecordOpType.Patch, diff2];
}
}
for (const removed of Object.keys(diff.removed)) {
if (!res) res = {};
res[removed] = [RecordOpType.Remove];
}
return res;
}
const ValueOpType = {
Put: "put",
Delete: "delete",
Append: "append",
Patch: "patch"
};
function diffRecord(prev, next, legacyAppendMode = false) {
return diffObject(prev, next, /* @__PURE__ */ new Set(["props", "meta"]), legacyAppendMode);
}
function diffObject(prev, next, nestedKeys, legacyAppendMode) {
if (prev === next) {
return null;
}
let result = null;
for (const key of Object.keys(prev)) {
if (!(key in next)) {
if (!result) result = {};
result[key] = [ValueOpType.Delete];
continue;
}
const prevValue = prev[key];
const nextValue = next[key];
if (nestedKeys?.has(key) || Array.isArray(prevValue) && Array.isArray(nextValue) || typeof prevValue === "string" && typeof nextValue === "string") {
const diff = diffValue(prevValue, nextValue, legacyAppendMode);
if (diff) {
if (!result) result = {};
result[key] = diff;
}
} else if (!isEqual(prevValue, nextValue)) {
if (!result) result = {};
result[key] = [ValueOpType.Put, nextValue];
}
}
for (const key of Object.keys(next)) {
if (!(key in prev)) {
if (!result) result = {};
result[key] = [ValueOpType.Put, next[key]];
}
}
return result;
}
function diffValue(valueA, valueB, legacyAppendMode) {
if (Object.is(valueA, valueB)) return null;
if (Array.isArray(valueA) && Array.isArray(valueB)) {
return diffArray(valueA, valueB, legacyAppendMode);
} else if (typeof valueA === "string" && typeof valueB === "string") {
if (!legacyAppendMode && valueB.startsWith(valueA)) {
const appendedText = valueB.slice(valueA.length);
return [ValueOpType.Append, appendedText, valueA.length];
}
return [ValueOpType.Put, valueB];
} else if (!valueA || !valueB || typeof valueA !== "object" || typeof valueB !== "object") {
return isEqual(valueA, valueB) ? null : [ValueOpType.Put, valueB];
} else {
const diff = diffObject(valueA, valueB, void 0, legacyAppendMode);
return diff ? [ValueOpType.Patch, diff] : null;
}
}
function diffArray(prevArray, nextArray, legacyAppendMode) {
if (Object.is(prevArray, nextArray)) return null;
if (prevArray.length === nextArray.length) {
const maxPatchIndexes = Math.max(prevArray.length / 5, 1);
const toPatchIndexes = [];
for (let i = 0; i < prevArray.length; i++) {
if (!isEqual(prevArray[i], nextArray[i])) {
toPatchIndexes.push(i);
if (toPatchIndexes.length > maxPatchIndexes) {
return [ValueOpType.Put, nextArray];
}
}
}
if (toPatchIndexes.length === 0) {
return null;
}
const diff = {};
for (const i of toPatchIndexes) {
const prevItem = prevArray[i];
const nextItem = nextArray[i];
if (!prevItem || !nextItem) {
diff[i] = [ValueOpType.Put, nextItem];
} else if (typeof prevItem === "object" && typeof nextItem === "object") {
const op = diffValue(prevItem, nextItem, legacyAppendMode);
if (op) {
diff[i] = op;
}
} else {
diff[i] = [ValueOpType.Put, nextItem];
}
}
return [ValueOpType.Patch, diff];
}
for (let i = 0; i < prevArray.length; i++) {
if (!isEqual(prevArray[i], nextArray[i])) {
return [ValueOpType.Put, nextArray];
}
}
return [ValueOpType.Append, nextArray.slice(prevArray.length), prevArray.length];
}
function applyObjectDiff(object, objectDiff) {
if (!object || typeof object !== "object") return object;
const isArray = Array.isArray(object);
let newObject = void 0;
const set = (k, v) => {
if (!newObject) {
if (isArray) {
newObject = [...object];
} else {
newObject = { ...object };
}
}
if (isArray) {
newObject[Number(k)] = v;
} else {
newObject[k] = v;
}
};
for (const [key, op] of Object.entries(objectDiff)) {
switch (op[0]) {
case ValueOpType.Put: {
const value = op[1];
if (!isEqual(object[key], value)) {
set(key, value);
}
break;
}
case ValueOpType.Append: {
const value = op[1];
const offset = op[2];
const currentValue = object[key];
if (Array.isArray(currentValue) && Array.isArray(value) && currentValue.length === offset) {
set(key, [...currentValue, ...value]);
} else if (typeof currentValue === "string" && typeof value === "string" && currentValue.length === offset) {
set(key, currentValue + value);
}
break;
}
case ValueOpType.Patch: {
if (object[key] && typeof object[key] === "object") {
const diff = op[1];
const patched = applyObjectDiff(object[key], diff);
if (patched !== object[key]) {
set(key, patched);
}
}
break;
}
case ValueOpType.Delete: {
if (key in object) {
if (!newObject) {
if (isArray) {
console.error("Can't delete array item yet (this should never happen)");
newObject = [...object];
} else {
newObject = { ...object };
}
}
delete newObject[key];
}
}
}
}
return newObject ?? object;
}
export {
RecordOpType,
ValueOpType,
applyObjectDiff,
diffRecord,
getNetworkDiff
};
//# sourceMappingURL=diff.mjs.map