UNPKG

@zedux/core

Version:

A high-level, declarative, composable form of Redux

151 lines (150 loc) 6.06 kB
import { BranchNodeType, NullNodeType } from '../utils/general.js'; /** * Turns a Hierarchy into a single reducer. * * All child HierarchyNodes must have `reducer` props themselves. * * Accepts configuration to create the state representation of this node, to get * and set properties on that data type, to determine if the old state is a * node, and to find the size of the node. */ const createBranchReducer = (children, { create, get, isNode, set, size }) => (oldState = create(), action) => { // Make a new node to keep track of the values returned by // the child reducers. let newState = create(); let hasChanges = false; // Iterate over the child reducers, passing them their state slice // and the action and recording their results. Object.keys(children).forEach(key => { const { reducer } = children[key]; // we've ensured reducer exists at this point // Grab the old state slice const oldStatePiece = isNode(oldState) ? get(oldState, key) : undefined; // yes, explicitly set it to undefined // Calculate the new value const newStatePiece = reducer(oldStatePiece, action); // Record the result newState = set(newState, key, newStatePiece); // Check for changes hasChanges || (hasChanges = newStatePiece !== oldStatePiece); }); // Handle the case where `children` did not used to be an empty node. This // means there were changes, but our change detection failed since we didn't // actually iterate over anything. hasChanges || (hasChanges = !isNode(oldState) || (!Object.keys(children).length && !!size(oldState))); // If nothing changed, discard the accumulated newState return hasChanges ? newState : oldState; }; /** * Recursively destroys a tree, preventing memory leaks. * * Currently STORE is the only node type affected by this; stores need to * unsubscribe() from their child stores. */ const destroyTree = (tree) => { if (!tree) return; const { children, destroy } = tree; if (destroy) destroy(); if (!children) return; // base case; this branch is now destroyed Object.values(children).forEach(destroyTree); }; /** * Merges two hierarchy BranchNodes together. * * Really should only be used from `mergeHierarchies()` */ const mergeBranches = (oldTree, newTree, hierarchyConfig) => { const mergedChildren = Object.assign({}, oldTree.children); // Iterate over the new tree's children Object.keys(newTree.children).forEach(key => { var _a; const newChild = newTree.children[key]; const oldChild = (_a = oldTree.children) === null || _a === void 0 ? void 0 : _a[key]; // Attempt to recursively merge the two children // Let `mergeHierarchies()` handle any destroying const mergedChild = mergeHierarchies(oldChild, newChild, hierarchyConfig); // If the new node is NULL, kill it. if (mergedChild.type === NullNodeType) { delete mergedChildren[key]; return; } mergedChildren[key] = mergedChild; }); return { children: mergedChildren, reducer: createBranchReducer(mergedChildren, hierarchyConfig), type: BranchNodeType, }; }; /** * Merges two hierarchies together. * * Uses head recursion to merge the leaf nodes first. This allows this step to * also find each node's reducer. (A node's children reducers need to exist * before its own reducer can) * * Destroys any no-longer-used resources in the oldTree. * * The resulting tree will always have the type of the newTree. * * Dynamically injects reducers and stores into the hierarchy or replaces the * hierarchy altogether. * * There are 4 types of nodes in this hierarchy: * - BRANCH - indicates a branch (non-leaf) node * - REDUCER - indicates a leaf node handled by this store * - STORE - indicates a leaf node handled by another store * - NULL - indicates a non-existent node, or node to be deleted * * BRANCH nodes will be deeply merged (recursively). * * All other nodes will be overwritten. */ export const mergeHierarchies = (oldTree, newTree, hierarchyConfig) => { if (newTree.type !== BranchNodeType) { destroyTree(oldTree); return newTree; } if (!oldTree || oldTree.type !== BranchNodeType) { destroyTree(oldTree); return mergeBranches({ type: NullNodeType }, newTree, hierarchyConfig); } // They're both BRANCH nodes; recursively merge them return mergeBranches(oldTree, newTree, hierarchyConfig); }; /** * Deeply merges the new state tree into the old one. * * If this hydration contains new state for a child store, this parent store * will create the child store's state for it :O * * This means that mixing hierarchyConfigs is not supported, since only the * parent's hierarchyConfig will be respected during this merge. The child's * state will be full-hydrated with its new state after this merge. */ export const mergeStateTrees = (oldStateTree, newStateTree, hierarchyConfig) => { if (!hierarchyConfig.isNode(oldStateTree) || !hierarchyConfig.isNode(newStateTree)) { return [newStateTree, newStateTree !== oldStateTree]; } let hasChanges = false; const mergedTree = hierarchyConfig.clone(oldStateTree); hierarchyConfig.iterate(newStateTree, (key, newVal) => { const oldVal = hierarchyConfig.get(mergedTree, key); const [clonedVal, childHasChanges] = hierarchyConfig.isNode(newVal) ? // Recursively merge the nested nodes. mergeStateTrees(oldVal, newVal, hierarchyConfig) : // Not a nested node (anymore, at least) [newVal, newVal !== oldVal]; if (!childHasChanges) return; if (!hasChanges) hasChanges = childHasChanges; hierarchyConfig.set(mergedTree, key, clonedVal); }); return [hasChanges ? mergedTree : oldStateTree, hasChanges]; };