repond
Version:
respond to items state in realtime
342 lines (287 loc) • 12.7 kB
text/typescript
import { forEach } from "chootils/dist/loops";
import { runWhenAddingAndRemovingItems, whenSettingStates } from "../helpers/runWhens";
import { repondMeta as meta } from "../meta";
import {
AllRefs,
AllState,
GetNewRefsByType,
GetNewStateByType,
ItemId,
ItemPropsByType,
ItemType,
PropId,
PropName,
PropValueFromPropId,
RepondCallback,
} from "../types";
import { applyPatch, getPatch } from "../usable/patchesAndDiffs";
export function setState<T extends PropId>(propPath: T, newValue: PropValueFromPropId<T>, itemId?: string) {
whenSettingStates(() => {
if (newValue === undefined) return;
if (!propPath) {
console.error("propPath must be provided");
return;
}
if (typeof propPath !== "string") {
console.error("propPath must be a string");
console.log("propPath", propPath);
return;
}
meta.recordedPropIdsChangedMap[meta.nowEffectPhase][propPath] = true;
meta.recordedPropIdsChangedMap.endOfStep[propPath] = true;
// if (meta.nowEffectPhase === "duringStep") {
// console.log("meta.nowEffectPhase", meta.nowEffectPhase);
// console.log(meta.recordedPropIdsChangedMap.duringStep?.[propPath]);
// }
const storeType = meta.itemTypeByPropPathId[propPath];
const propKey = meta.propKeyByPropPathId[propPath];
let foundItemId = itemId || meta.itemIdsByItemType[storeType]?.[0];
if (!foundItemId) {
foundItemId = Object.keys(meta.nowState[storeType] ?? {})[0];
console.warn(
`${propPath}No itemId found for setState ${storeType}, using first found itemId: ${foundItemId} from Object keys`
);
}
const recordedChanges =
meta.nowEffectPhase === "duringStep" ? meta.recordedEffectChanges : meta.recordedStepEndEffectChanges;
const allRecordedChanges = meta.recordedStepEndEffectChanges;
// check if the item exists before copying
if (meta.nowState?.[storeType]?.[foundItemId] === undefined) return;
// save the new state
meta.nowState[storeType][foundItemId][propKey] = newValue;
if (meta.nowEffectPhase === "duringStep") {
recordedChanges.itemTypesBool[storeType] = true;
if (!recordedChanges.itemIdsBool[storeType]) {
recordedChanges.itemIdsBool[storeType] = {};
}
recordedChanges.itemIdsBool[storeType][foundItemId] = true;
if (!recordedChanges.itemPropsBool[storeType][foundItemId]) {
recordedChanges.itemPropsBool[storeType][foundItemId] = {};
}
recordedChanges.itemPropsBool[storeType][foundItemId][propKey] = true;
recordedChanges.somethingChanged = true;
}
allRecordedChanges.itemTypesBool[storeType] = true;
if (!allRecordedChanges.itemIdsBool[storeType]) {
allRecordedChanges.itemIdsBool[storeType] = {};
}
allRecordedChanges.itemIdsBool[storeType][foundItemId] = true;
if (!allRecordedChanges.itemPropsBool[storeType][foundItemId]) {
allRecordedChanges.itemPropsBool[storeType][foundItemId] = {};
}
allRecordedChanges.itemPropsBool[storeType][foundItemId][propKey] = true;
allRecordedChanges.somethingChanged = true;
});
}
export function setNestedState(newState: Partial<AllState>) {
whenSettingStates(() => {
if (!newState) return;
const itemTypes = Object.keys(newState) as ItemType[];
forEach(itemTypes, (itemType) => {
const itemIds = Object.keys(newState[itemType] as any);
forEach(itemIds, (itemId) => {
const itemProps = Object.keys(newState[itemType]![itemId]);
forEach(itemProps, (propName) => {
const newValue = newState[itemType]![itemId][propName];
setState(`${itemType}.${propName}`, newValue, itemId);
});
});
});
});
}
export const getNewState = <T_Type extends ItemType>(itemType: T_Type): GetNewStateByType[T_Type] =>
meta.newStateByItemType[itemType] as GetNewStateByType[T_Type];
export const getNewRefs = <T_Type extends ItemType>(itemType: T_Type): GetNewRefsByType[T_Type] =>
meta.newRefsByItemType[itemType] as GetNewRefsByType[T_Type];
export const getItemTypes = (): ItemType[] => meta.itemTypeNames;
export const getItemIds = (itemType: ItemType): string[] => meta.itemIdsByItemType[itemType];
const _getNestedState = (): AllState => meta.nowState as AllState;
export const getState = <T_Type extends ItemType>(itemType: T_Type, itemId?: string): AllState[T_Type][string] => {
if (!itemId) {
const foundItemId = meta.itemIdsByItemType?.[itemType]?.[0];
if (!foundItemId)
console.warn(`(getState) No itemId provided for ${itemType}, using first found itemId: ${foundItemId}`);
return meta.nowState[itemType][foundItemId];
}
// const allItemTypeState = meta.nowState[kind];
// if (allItemTypeState === undefined) {
// console.warn(`(getState) No state found for ${kind}`);
// }
// const foundState = allItemTypeState?.[itemId];
// if (foundState === undefined) {
// console.warn(`(getState) No state found for ${kind} with id ${itemId}`);
// }
// return foundState;
return meta.nowState[itemType]?.[itemId];
};
// Good for running things to be sure the state change is seen
export function onNextTick(callback?: RepondCallback) {
if (callback) meta.nextTickQueue.push(callback);
}
export const getPrevState = <T_ItemType extends ItemType>(
itemType: T_ItemType,
itemId?: string
): AllState[T_ItemType][keyof AllState[T_ItemType]] => {
if (!itemId) {
// const foundItemId = meta.prevItemIdsByItemType?.[kind]?.[0];
const foundItemId = Object.keys(meta.prevState?.[itemType] ?? {})?.[0] ?? meta.itemIdsByItemType?.[itemType]?.[0];
if (!foundItemId) {
// console.warn(`(getPrevState) No itemId provided for ${kind}, using first found itemId: ${foundItemId}`);
}
return meta.prevState?.[itemType]?.[foundItemId] ?? meta.nowState[itemType][foundItemId];
}
if (!meta.prevState[itemType]?.[itemId]) {
// console.warn(`(getPrevState) No prevState found for ${kind} with id ${itemId} (using nowState instead)`);
return meta.nowState[itemType][itemId];
}
return meta.prevState[itemType][itemId];
};
export const getRefs = <T_ItemType extends ItemType>(
itemType: T_ItemType,
itemId?: string
): AllRefs[T_ItemType][keyof AllRefs[T_ItemType]] => {
if (!itemId) {
const foundItemId = meta.itemIdsByItemType?.[itemType]?.[0];
if (!foundItemId) {
console.warn(`(getRefs) No itemId provided for ${itemType}, using first found itemId: ${foundItemId}`);
}
return meta.nowRefs[itemType][foundItemId];
}
if (meta.nowRefs?.[itemType]?.[itemId] === undefined) {
console.warn(`(getRefs) No refs found for ${itemType} with id ${itemId}`);
}
return meta.nowRefs[itemType][itemId];
};
// Adding and removing items
export function addItem<T_ItemType extends ItemType>(
type: T_ItemType,
id: string,
state?: Partial<AllState[T_ItemType][ItemId]>,
refs?: Partial<AllRefs[T_ItemType][ItemId]>
) {
if (!meta.willAddItemsInfo[type]) meta.willAddItemsInfo[type] = {};
meta.willAddItemsInfo[type][id] = true;
const propPath = `${type}.__added`;
runWhenAddingAndRemovingItems(() => {
meta.nowState[type][id] = {
...meta.newStateByItemType[type](id),
...(state || {}),
};
meta.nowRefs[type][id] = {
...meta.newRefsByItemType[type]?.(id, meta.nowState[type][id]),
...(refs || {}),
};
meta.itemIdsByItemType[type].push(id);
meta.recordedStepEndEffectChanges.itemTypesBool[type] = true;
// TODO Figure out if adding an item should record the properties as changed or not?
meta.recordedStepEndEffectChanges.itemPropsBool[type][id] = {};
meta.recordedEffectChanges.itemPropsBool[type][id] = {};
meta.diffInfo.propsChanged[type as ItemType][id] = [];
meta.diffInfo.propsChangedBool[type as ItemType][id] = {};
meta.recordedStepEndEffectChanges.itemIdsBool[type][id] = true;
meta.recordedStepEndEffectChanges.somethingChanged = true;
meta.recordedEffectChanges.itemTypesBool[type] = true;
meta.recordedEffectChanges.itemIdsBool[type][id] = true;
meta.recordedEffectChanges.somethingChanged = true;
meta.recordedPropIdsChangedMap[meta.nowEffectPhase][propPath] = true;
meta.recordedPropIdsChangedMap.endOfStep[propPath] = true;
// NOTE new items with props different to the defaults props are recorded as changed
const itemPropNames = meta.propNamesByItemType[type];
forEach(itemPropNames, (propName) => {
const propChangedFromDefault = meta.nowState[type][id][propName] !== meta.newStateByItemType[type](id)[propName];
if (propChangedFromDefault) {
meta.recordedStepEndEffectChanges.itemPropsBool[type][id][propName] = true;
meta.recordedEffectChanges.itemPropsBool[type][id][propName] = true;
}
});
});
}
export function removeItem(type: ItemType, id: string) {
if (!meta.willRemoveItemsInfo[type]) meta.willRemoveItemsInfo[type] = {};
meta.willRemoveItemsInfo[type][id] = true;
const propPath = `${type}.__added`;
runWhenAddingAndRemovingItems(() => {
// removing itemId
delete meta.nowState[type][id];
meta.itemIdsByItemType[type] = Object.keys(meta.nowState[type]);
// delete meta.currentRefs[itemType][itemId]; // now done at the end of update repond
meta.recordedStepEndEffectChanges.itemTypesBool[type] = true;
meta.recordedStepEndEffectChanges.somethingChanged = true;
meta.recordedEffectChanges.itemTypesBool[type] = true;
meta.recordedEffectChanges.somethingChanged = true;
meta.recordedPropIdsChangedMap[meta.nowEffectPhase][propPath] = true;
meta.recordedPropIdsChangedMap.endOfStep[propPath] = true;
});
}
export function getItemWillBeAdded<T_Type extends ItemType>(type: T_Type, id: string) {
return !!meta.willAddItemsInfo[type]?.[id];
}
export function getItemWillBeRemoved<T_Type extends ItemType>(type: T_Type, id: string) {
return !!meta.willRemoveItemsInfo[type]?.[id] || !!(meta.nowState as any)[type][id];
}
export function getItemWillExist<T_Type extends ItemType>(type: T_Type, id: string) {
return getItemWillBeAdded(type, id) || (!!getState(type, id) as any);
}
// For saving and loading
// Function to selectively get data with only specific props from the repond store, can be used for save data
export function getPartialState_OLD(propsToGet: Partial<ItemPropsByType>) {
const itemTypes = Object.keys(propsToGet) as Array<keyof ItemPropsByType>;
if (!meta.didInit) {
console.warn("getPartialState called before repond was initialized");
return {};
}
const partialState: Partial<AllState> = {};
for (const itemType of itemTypes) {
const itemPropNames = propsToGet[itemType]!;
const itemIds = meta.itemIdsByItemType[itemType];
const partialItems: Record<string, any> = {};
for (const itemId of itemIds) {
const item = getState(itemType, itemId);
const partialItem: Record<string, any> = {};
for (const propName of itemPropNames) {
partialItem[propName] = item[propName];
}
partialItems[itemId] = partialItem;
}
partialState[itemType] = partialItems as any;
}
return partialState as Partial<AllState>;
}
export function getPartialState(propsToGet: PropId[]) {
const itemType = meta.itemTypeByPropPathId;
if (!meta.didInit) {
console.warn("getPartialState called before repond was initialized");
return {};
}
const partialState: Partial<AllState> = {};
const propsByItemType = {} as Record<ItemType, PropName<ItemType>[]>;
for (const propId of propsToGet) {
const itemType = meta.itemTypeByPropPathId[propId];
if (!itemType) {
console.log("propId has no item type", propId);
continue;
}
const propName = meta.propKeyByPropPathId[propId] as PropName<ItemType>;
if (!propsByItemType[itemType]) propsByItemType[itemType] = [];
propsByItemType[itemType].push(propName);
}
const itemTypes = Object.keys(propsByItemType) as ItemType[];
for (const itemType of itemTypes) {
const itemIds = meta.itemIdsByItemType[itemType];
const partialItems: Record<string, any> = {};
const itemPropNames = propsByItemType[itemType];
for (const itemId of itemIds) {
const item = getState(itemType, itemId);
const partialItem: Record<string, any> = {};
for (const propName of itemPropNames) {
partialItem[propName] = item[propName];
}
partialItems[itemId] = partialItem;
}
partialState[itemType] = partialItems as any;
}
return partialState as Partial<AllState>;
}
export function applyState(partialState: Partial<AllState>) {
if (partialState) applyPatch(getPatch(_getNestedState(), partialState));
}