repond
Version:
respond to items state in realtime
482 lines (481 loc) • 22.8 kB
JavaScript
import { getUniqueArrayItems } from "chootils/dist/arrays";
import { forEach } from "chootils/dist/loops";
import { cloneObjectWithJson, copyStates } from "../copyStates";
import { createDiffInfo, getStatesDiff } from "../getStatesDiff";
import { EMPTY_RECORDED_CHANGES } from "../meta";
import { addItem, getNewState, getItemTypes, removeItem, setNestedState } from "./getSet";
export function makeEmptyPatch() {
return {
changed: {},
added: {},
removed: {},
};
}
export function makeEmptyDiff() {
return {
changedNext: {},
changedPrev: {},
added: {},
removed: {},
};
}
function makeEmptyDiffInfo() {
const emptyDiffInfo = {
itemTypesChanged: [],
itemTypesWithAdded: [],
itemTypesWithRemoved: [],
itemsChanged: { __all: [] },
propsChanged: {},
itemsAdded: { __all: [] },
itemsRemoved: { __all: [] },
itemTypesChangedBool: {},
itemTypesWithAddedBool: {},
itemTypesWithRemovedBool: {},
itemsChangedBool: {},
propsChangedBool: {},
itemsAddedBool: {},
itemsRemovedBool: {},
};
createDiffInfo(emptyDiffInfo);
return emptyDiffInfo;
}
export function applyPatch(patch) {
const itemTypes = getItemTypes();
forEach(itemTypes, (type) => {
forEach(patch.removed[type] ?? [], (id) => removeItem(type, id));
forEach(patch.added[type] ?? [], (id) => addItem(type, id, patch.changed?.[type]?.[id]));
});
setNestedState(patch.changed);
}
export function applyPatchHere(newStates, patch) {
const itemTypes = getItemTypes();
forEach(itemTypes, (type) => {
// Loop through each removed item, and delete it from newStates
forEach(patch.removed[type] ?? [], (id) => {
const itemTypeState = newStates[type];
if (itemTypeState && itemTypeState[id]) {
delete itemTypeState[id];
}
});
// Loop through each new item, and add it to newStates with getDefaultState(itemId)
forEach(patch.added[type] ?? [], (id) => {
if (!newStates[type]) {
newStates[type] = {};
}
const itemTypeState = newStates[type];
if (itemTypeState) {
if (itemTypeState[id] === undefined) {
itemTypeState[id] = getNewState(type)(id); // NOTE maybe no need to add it then delete it?
}
}
if (itemTypeState && itemTypeState[id]) {
delete itemTypeState[id];
}
});
// Loop through each changed items and set the properties in newState
const changedItemsForType = patch.changed[type];
if (changedItemsForType !== undefined) {
const changedItemIds = Object.keys(changedItemsForType);
forEach(changedItemIds, (itemId) => {
const changedPropertiesForItem = changedItemsForType[itemId];
const storeState = newStates[type];
if (changedPropertiesForItem !== undefined && storeState !== undefined) {
const itemState = storeState[itemId];
if (itemState !== undefined) {
const changePropertyNames = Object.keys(changedPropertiesForItem);
forEach(changePropertyNames, (propertyName) => {
storeState[itemId] = changedPropertiesForItem[propertyName];
});
}
}
});
}
});
}
function getPatchOrDiff(prevState, newState, patchOrDiff) {
const itemTypes = getItemTypes();
const newPatch = makeEmptyPatch();
const tempDiffInfo = makeEmptyDiffInfo();
try {
getStatesDiff(newState, // currentState
prevState, // previousState
tempDiffInfo, EMPTY_RECORDED_CHANGES, // manualUpdateChanges
true // checkAllChanges
);
}
catch (error) {
console.log("Error");
console.log(error);
}
// Then can use tempDiffInfo to make the patch (with items removed etc)
forEach(itemTypes, (itemType) => {
// Add added and removed with itemsAdded and itemsRemoved
if (tempDiffInfo.itemsAdded[itemType] && tempDiffInfo.itemsAdded[itemType].length > 0) {
newPatch.added[itemType] = tempDiffInfo.itemsAdded[itemType];
}
if (tempDiffInfo.itemsRemoved[itemType] && tempDiffInfo.itemsRemoved[itemType].length > 0) {
newPatch.removed[itemType] = tempDiffInfo.itemsRemoved[itemType];
}
});
// To add changed
// Loop through items changes, then itemIdsChanged[itemType] then
// propsChanged[itemType][itemId]
// And set the changed to the value in newState
forEach(tempDiffInfo.itemTypesChanged, (itemType) => {
if (!newPatch.changed[itemType]) {
newPatch.changed[itemType] = {};
}
const patchChangesForItemType = newPatch.changed[itemType];
if (patchChangesForItemType) {
forEach(tempDiffInfo.itemsChanged[itemType], (itemId) => {
if (!patchChangesForItemType[itemId]) {
patchChangesForItemType[itemId] = {};
}
const patchChangesForItemId = patchChangesForItemType[itemId];
if (patchChangesForItemId) {
const propsChangedForType = tempDiffInfo.propsChanged[itemType];
forEach(propsChangedForType[itemId], (propertyName) => {
patchChangesForItemId[propertyName] = newState?.[itemType]?.[itemId]?.[propertyName];
});
}
});
}
});
// Need to also add non-default properties for new items to changed
// For each item added,
// const defaultState = defaultStates[itemType](itemId)
// const newState = newState[itemType][itemId]
// Loop through each property and compare
// If they’re different, add it to the changed object
forEach(itemTypes, (itemType) => {
if (newPatch?.added[itemType]?.length) {
const itemIdsAddedForType = newPatch.added[itemType];
const newItemTypeState = newState[itemType];
let propertyNamesForItemType = [];
let propertyNamesHaveBeenFound = false;
forEach(itemIdsAddedForType ?? [], (itemId) => {
const defaultItemState = getNewState(itemType)(itemId);
const addedItemState = newItemTypeState?.[itemId];
if (!propertyNamesHaveBeenFound) {
propertyNamesForItemType = Object.keys(defaultItemState);
propertyNamesHaveBeenFound = true;
}
if (addedItemState) {
forEach(propertyNamesForItemType, (propertyName) => {
const defaultPropertyValue = defaultItemState[propertyName];
const newPropertyValue = addedItemState?.[propertyName];
if (defaultPropertyValue !== undefined && newPropertyValue !== undefined) {
let valuesAreTheSame = defaultPropertyValue === newPropertyValue;
if (typeof newPropertyValue === "object") {
valuesAreTheSame = JSON.stringify(defaultPropertyValue) === JSON.stringify(newPropertyValue);
}
if (!valuesAreTheSame) {
if (!newPatch.changed[itemType]) {
newPatch.changed[itemType] = {};
}
const newPatchChangedForItemType = newPatch.changed[itemType];
if (newPatchChangedForItemType) {
if (!newPatchChangedForItemType[itemId]) {
newPatchChangedForItemType[itemId] = {};
}
const newPatchChangedForItemId = newPatchChangedForItemType[itemId];
if (newPatchChangedForItemId) {
newPatchChangedForItemId[propertyName] = newPropertyValue;
}
}
}
}
});
}
});
}
});
if (patchOrDiff === "patch") {
return newPatch;
}
const newDiff = makeEmptyDiff();
newDiff.added = newPatch.added;
newDiff.removed = newPatch.removed;
newDiff.changedNext = newPatch.changed;
// Need to also add non-default properties for removed items to changedPrev
// For each item removed,
// const defaultState = defaultStates[itemType](itemId)
// const newState = prevState[itemType][itemId]
// Loop through each property and compare
// If they’re different, add it to the changedPrev object
// (same as for added, but instead of adding to newPatch.changed, it's to newDiff.changedPrev, and checking the prevState)
forEach(itemTypes, (itemType) => {
if (newDiff.removed[itemType]?.length) {
const itemIdsRemovedForType = newDiff.removed[itemType];
const prevItemTypeState = prevState[itemType];
let propertyNamesForItemType = [];
let propertyNamesHaveBeenFound = false;
forEach(itemIdsRemovedForType ?? [], (itemId) => {
const defaultItemState = getNewState(itemType)(itemId);
const removedItemState = prevItemTypeState?.[itemId];
if (!propertyNamesHaveBeenFound) {
propertyNamesForItemType = Object.keys(defaultItemState);
propertyNamesHaveBeenFound = true;
}
if (removedItemState) {
forEach(propertyNamesForItemType, (propertyName) => {
const defaultPropertyValue = defaultItemState[propertyName];
const newPropertyValue = removedItemState?.[propertyName];
if (defaultPropertyValue !== undefined && newPropertyValue !== undefined) {
let valuesAreTheSame = removedItemState[propertyName] === newPropertyValue;
if (typeof newPropertyValue === "object") {
valuesAreTheSame = JSON.stringify(defaultPropertyValue) === JSON.stringify(newPropertyValue);
}
if (!valuesAreTheSame) {
if (!newDiff.changedPrev[itemType]) {
newDiff.changedPrev[itemType] = {};
}
const newDiffChangedForItemType = newDiff.changedPrev[itemType];
if (newDiffChangedForItemType) {
if (!newDiffChangedForItemType[itemId]) {
newDiffChangedForItemType[itemId] = {};
}
const newDiffChangedForItemId = newDiffChangedForItemType[itemId];
if (newDiffChangedForItemId) {
newDiffChangedForItemId[propertyName] = newPropertyValue;
}
}
}
}
});
}
});
}
});
return newDiff;
}
export function getPatch(prevState, newState) {
return getPatchOrDiff(prevState, newState, "patch");
}
export function getPatchAndReversed(prevState, newState) {
const patch = getPatch(prevState, newState);
const reversePatch = getPatch(newState, prevState);
return [patch, reversePatch];
}
export function getReversePatch(partialState, newPatch) {
const prevState = {};
copyStates(partialState, prevState);
const newState = {};
copyStates(partialState, newState);
applyPatchHere(newState, newPatch);
const reversePatch = getPatch(newState, prevState);
return reversePatch;
}
export function combineTwoPatches(prevPatch, newPatch) {
const itemTypes = getItemTypes();
const combinedPatch = makeEmptyPatch();
//
forEach(itemTypes, (itemType) => {
// combine added and removed , and remove duplicates
const itemsAddedPrev = prevPatch.added[itemType];
const itemsAddedNew = newPatch.added[itemType];
const hasAddedItems = itemsAddedPrev?.length || itemsAddedNew?.length;
if (hasAddedItems) {
combinedPatch.added[itemType] = getUniqueArrayItems([
...(itemsAddedPrev ?? []),
...(itemsAddedNew ?? []),
]);
}
const itemsRemovedPrev = prevPatch.removed[itemType];
const itemsRemovedNew = newPatch.removed[itemType];
const hasRemovedItems = (itemsRemovedPrev && itemsRemovedPrev.length > 0) || (itemsRemovedNew && itemsRemovedNew.length > 0);
if (hasRemovedItems) {
combinedPatch.removed[itemType] = getUniqueArrayItems([
...(itemsRemovedPrev ?? []),
...(itemsRemovedNew ?? []),
]);
}
// Anything in removed in prev that was added in new, removed from removed
if (itemsRemovedPrev && itemsAddedNew) {
combinedPatch.removed[itemType] = combinedPatch.removed[itemType].filter((itemId) => {
if (itemsRemovedPrev.includes(itemId) && itemsAddedNew.includes(itemId)) {
return false;
}
return true;
});
}
// Anything in removed in new that was added in prev, removed from added
if (itemsRemovedNew && itemsAddedPrev) {
combinedPatch.added[itemType] = combinedPatch.added[itemType].filter((itemId) => {
if (itemsRemovedNew.includes(itemId) && itemsAddedPrev.includes(itemId)) {
return false;
}
return true;
});
}
// Merge changes
const itemsChangedPrev = prevPatch.changed[itemType];
const itemsChangedNew = prevPatch.changed[itemType];
const hasChangedItems = itemsChangedPrev || itemsChangedNew;
if (hasChangedItems) {
const allChangedItemIds = Object.keys({
...(itemsChangedPrev ?? {}),
...(itemsChangedNew ?? {}),
});
if (!combinedPatch.changed[itemType]) {
combinedPatch.changed[itemType] = {};
}
const combinedPatchChangedForItemType = combinedPatch.changed[itemType];
if (combinedPatchChangedForItemType) {
forEach(allChangedItemIds, (itemId) => {
const combinedPatchChangedForItemId = combinedPatchChangedForItemType[itemId];
combinedPatchChangedForItemType[itemId] = {
...(itemsChangedPrev?.[itemId] ?? {}),
...(itemsChangedNew?.[itemId] ?? {}),
};
});
// Remove any item changes that are in removed
forEach(combinedPatch.removed[itemType] ?? [], (itemId) => {
if (combinedPatchChangedForItemType[itemId]) {
delete combinedPatchChangedForItemType[itemId];
}
});
}
}
});
return combinedPatch;
}
export function combinePatches(patchesArray) {
let combinedPatches = patchesArray[0];
forEach(patchesArray, (loopedPatch, index) => {
const currentPatch = combinedPatches;
const nextPatch = patchesArray[index + 1];
if (nextPatch) {
combinedPatches = combineTwoPatches(currentPatch, nextPatch);
}
});
return combinedPatches;
}
export function makeMinimalPatch(currentStates, thePatch) {
const itemTypes = getItemTypes();
const minimalPatch = cloneObjectWithJson(thePatch);
// Loop through the changed items, and each changed property
forEach(itemTypes, (itemType) => {
const propertyNames = Object.keys(getNewState(itemType)("anItemId"));
const changedForType = minimalPatch.changed[itemType];
if (changedForType) {
const changedItemIds = Object.keys(changedForType ?? {});
forEach(changedItemIds, (itemId) => {
const changedForItem = changedForType[itemId];
const itemState = currentStates?.[itemType]?.[itemId];
if (changedForItem && itemState) {
forEach(propertyNames, (propertyName) => {
// If the value’s the same as state, remove that change property
if (changedForItem[propertyName] === itemState[propertyName]) {
delete changedForItem[propertyName];
}
});
}
// (if the item has no more properties, remove that changed item)
const changedPropertyNames = Object.keys(changedForItem ?? {});
if (changedPropertyNames.length === 0) {
delete changedForType[itemId];
}
});
}
// Loop through the added items, if the item already exists in state, remove it from added
if (minimalPatch.added[itemType]) {
minimalPatch.added[itemType] = minimalPatch.added[itemType].filter((itemId) => !!currentStates?.[itemType]?.[itemId]);
}
// Loop through the removed items, if the item doesn’t exist in state, remove it from removed
if (minimalPatch.removed[itemType]) {
minimalPatch.removed[itemType] = minimalPatch.removed[itemType].filter((itemId) => !currentStates?.[itemType]?.[itemId]);
}
});
}
export function removePartialPatch(thePatch, patchToRemove) {
const newPatch = cloneObjectWithJson(thePatch);
const itemTypes = getItemTypes();
forEach(itemTypes, (itemType) => {
// Loop through removed in patchToRemove, if it’s in newPatch , remove it
if (newPatch.removed[itemType]) {
newPatch.removed[itemType] = newPatch.removed[itemType].filter((itemId) => !patchToRemove.removed[itemType].includes(itemId));
}
// Loop through added in patchToRemove, if it’s in newPatch , remove it
// Keep track of noLongerAddedItems { itemType: []
const noLongerAddedItems = [];
if (newPatch.added[itemType]) {
newPatch.added[itemType] = newPatch.added[itemType].filter((itemId) => {
const shouldKeep = !patchToRemove.added[itemType].includes(itemId);
if (!shouldKeep) {
noLongerAddedItems.push(itemId);
}
return shouldKeep;
});
}
// Loop through changed items, if the same change is in patchToRemove and newPatch, remove it from new patch
const removedPatchChangedForType = patchToRemove.changed[itemType];
const newPatchChangedForType = newPatch.changed[itemType];
if (removedPatchChangedForType && newPatchChangedForType) {
const changedItemIds = Object.keys(removedPatchChangedForType ?? {});
forEach(changedItemIds, (itemId) => {
const removedPatchChangedForItem = removedPatchChangedForType[itemId];
const newPatchChangedForItem = newPatchChangedForType[itemId];
if (removedPatchChangedForItem && newPatchChangedForItem) {
// (if the item has no more properties, remove that changed item)
const removedPatchChangedPropertyNames = Object.keys(removedPatchChangedForItem ?? {});
forEach(removedPatchChangedPropertyNames, (propertyName) => {
if (JSON.stringify(removedPatchChangedForItem[propertyName]) ===
JSON.stringify(newPatchChangedForItem[propertyName])) {
delete newPatchChangedForItem[propertyName];
}
});
}
const changedPropertyNamesB = Object.keys(removedPatchChangedForItem ?? {});
// If there's no more property changes for an item name, or that item isn't added anymore, then remove it from changes
const noMorePropertyChanges = changedPropertyNamesB.length === 0;
if (noMorePropertyChanges || noLongerAddedItems.includes(itemId)) {
delete newPatchChangedForType[itemId];
}
});
}
});
}
export function getDiff(prevState, newState) {
return getPatchOrDiff(prevState, newState, "diff");
}
export function getDiffFromPatches(forwardPatch, reversePatch) {
const newDiff = makeEmptyDiff();
newDiff.added = forwardPatch.added;
newDiff.removed = forwardPatch.removed;
newDiff.changedNext = forwardPatch.changed;
newDiff.changedPrev = reversePatch.changed;
//
//
// Maybe if forwardPatch.added/ removed isnt same as backwardPatch removed/added then show warning, but use the forward patch?
return newDiff;
}
export function getPatchesFromDiff(theDiff) {
const forwardPatch = makeEmptyPatch();
const reversePatch = makeEmptyPatch();
forwardPatch.added = theDiff.added;
forwardPatch.removed = theDiff.removed;
reversePatch.added = theDiff.removed;
reversePatch.removed = theDiff.added;
forwardPatch.changed = theDiff.changedNext;
reversePatch.changed = theDiff.changedPrev;
return [forwardPatch, reversePatch];
}
export function combineTwoDiffs(prevDiff, newDiff) {
const [prevForwardPatch, prevReversePatch] = getPatchesFromDiff(prevDiff);
const [newForwardPatch, newReversePatch] = getPatchesFromDiff(prevDiff);
const combinedForwardPatch = combineTwoPatches(prevForwardPatch, newForwardPatch);
const combinedReversePatch = combineTwoPatches(prevReversePatch, newReversePatch);
return getDiffFromPatches(combinedForwardPatch, combinedReversePatch);
}
export function combineDiffs(diffsArray) {
let combinedDiffs = diffsArray[0];
forEach(diffsArray, (loopedDiff, index) => {
const currentDiff = combinedDiffs;
const nextDiff = diffsArray[index + 1];
if (nextDiff) {
combinedDiffs = combineTwoDiffs(currentDiff, nextDiff);
}
});
return combinedDiffs;
}