// Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { createProxy, getUntracked, isChanged } from 'proxy-compare'; export type ExcludeNull = Exclude; export type ProxyMemoizeOptions = Readonly<{ // For debugging name: string; equalityFn?: (prev: Result, next: Result) => boolean; }>; export function proxyMemoize, Result>( fn: (...params: Params) => ExcludeNull, { equalityFn }: ProxyMemoizeOptions> ): (...param: Params) => ExcludeNull { type CacheEntryType = Readonly<{ params: Params; result: ExcludeNull; }>; const cache = new WeakMap(); const affected = new WeakMap(); const proxyCache = new WeakMap(); const changedCache = new WeakMap(); return (...params: Params): ExcludeNull => { if (params.length < 1) { throw new Error('At least one parameter is required'); } const cacheKey = params[0]; const entry = cache.get(cacheKey); if (entry && entry.params.length === params.length) { let isValid = true; for (const [i, cachedParam] of entry.params.entries()) { // Proxy wasn't even touched - we are good to go. const wasUsed = affected.has(cachedParam); if (!wasUsed) { continue; } if (isChanged(cachedParam, params[i], affected, changedCache)) { isValid = false; break; } } if (isValid) { return entry.result; } } const proxies = params.map(param => createProxy(param, affected, proxyCache) ) as unknown as Params; const trackedResult = fn(...proxies); const untrackedResult = getUntracked(trackedResult); // eslint-disable-next-line eqeqeq let result = untrackedResult === null ? trackedResult : untrackedResult; // Try to reuse result if custom equality check is configured. if (entry && equalityFn && equalityFn(entry.result, result)) { ({ result } = entry); } cache.set(cacheKey, { params, result, }); return result; }; }