import * as trailingSlash from "../trailingSlash.js"; const treeToCaches = new WeakMap(); /** * Given a key function, return a new key function and inverse key function that * cache the results of the original. * * If `skipSubtrees` is true, the inverse key function will skip any source keys * that are keys for subtrees, returning the source key unmodified. * * @typedef {import("../../index.ts").KeyFn} KeyFn * * @param {KeyFn} keyFn * @param {boolean?} skipSubtrees * @returns {{ key: KeyFn, inverseKey: KeyFn }} */ export default function cachedKeyFunctions(keyFn, skipSubtrees = false) { return { async inverseKey(resultKey, tree) { const { resultKeyToSourceKey, sourceKeyToResultKey } = getKeyMapsForTree(tree); const cachedSourceKey = searchKeyMap(resultKeyToSourceKey, resultKey); if (cachedSourceKey !== undefined) { return cachedSourceKey; } // Iterate through the tree's keys, calculating source keys as we go, // until we find a match. Cache all the intermediate results and the // final match. This is O(n), but we stop as soon as we find a match, // and subsequent calls will benefit from the intermediate results. const resultKeyWithoutSlash = trailingSlash.remove(resultKey); for (const sourceKey of await tree.keys()) { // Skip any source keys we already know about. if (sourceKeyToResultKey.has(sourceKey)) { continue; } const computedResultKey = await computeAndCacheResultKey( tree, keyFn, skipSubtrees, sourceKey ); if ( computedResultKey && trailingSlash.remove(computedResultKey) === resultKeyWithoutSlash ) { // Match found, match trailing slash and return return trailingSlash.toggle(sourceKey, trailingSlash.has(resultKey)); } } return undefined; }, async key(sourceKey, tree) { const { sourceKeyToResultKey } = getKeyMapsForTree(tree); const cachedResultKey = searchKeyMap(sourceKeyToResultKey, sourceKey); if (cachedResultKey !== undefined) { return cachedResultKey; } const resultKey = await computeAndCacheResultKey( tree, keyFn, skipSubtrees, sourceKey ); return resultKey; }, }; } async function computeAndCacheResultKey(tree, keyFn, skipSubtrees, sourceKey) { const { resultKeyToSourceKey, sourceKeyToResultKey } = getKeyMapsForTree(tree); const resultKey = skipSubtrees && trailingSlash.has(sourceKey) ? sourceKey : await keyFn(sourceKey, tree); sourceKeyToResultKey.set(sourceKey, resultKey); resultKeyToSourceKey.set(resultKey, sourceKey); return resultKey; } // Maintain key->inverseKey and inverseKey->key mappings for each tree. These // store subtree keys in either direction with a trailing slash. function getKeyMapsForTree(tree) { let keyMaps = treeToCaches.get(tree); if (!keyMaps) { keyMaps = { resultKeyToSourceKey: new Map(), sourceKeyToResultKey: new Map(), }; treeToCaches.set(tree, keyMaps); } return keyMaps; } // Search the given key map for the key. Ignore trailing slashes in the search, // but preserve them in the result. function searchKeyMap(keyMap, key) { // Check key as is let match; if (keyMap.has(key)) { match = keyMap.get(key); } else if (!trailingSlash.has(key)) { // Check key without trailing slash const withSlash = trailingSlash.add(key); if (keyMap.has(withSlash)) { match = keyMap.get(withSlash); } } return match ? trailingSlash.toggle(match, trailingSlash.has(key)) : undefined; }