| // Copyright (C) 2026 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * Memo: Synchronous memoization keyed by a JSON-serializable value. |
| * |
| * ## Why it exists |
| * |
| * Components often need to compute derived values that are expensive to |
| * calculate but stable across render cycles when inputs haven't changed. |
| * Memo provides a single-entry cache keyed by a JSON-serializable object: |
| * - Call `use()` every render cycle with the current key and compute function |
| * - Get back the cached value if the key hasn't changed |
| * - Recompute automatically when the key changes |
| * |
| * ## Usage |
| * |
| * ```typescript |
| * class MyPanel implements m.ClassComponent<Attrs> { |
| * private readonly computedCache = new Memo<MyData>(); |
| * |
| * view({attrs}: m.CVnode<Attrs>) { |
| * const data = this.computedCache.use({ |
| * key: {filters: attrs.filters, count: this.count}, |
| * compute: () => expensiveCompute(attrs.filters, this.count), |
| * }); |
| * |
| * return m('div', renderData(data)); |
| * } |
| * } |
| * ``` |
| * |
| * ## Key concepts |
| * |
| * - **key**: JSON-serializable object identifying the computation. Changes |
| * trigger a recompute. Supports primitives, arrays, objects, and bigints. |
| * - **compute**: Synchronous function that returns the value to cache. |
| * Called only when the key changes. |
| * - **disposal**: Cached `Disposable` values are disposed when replaced, |
| * invalidated, or when the memo itself is disposed. |
| */ |
| |
| import {isDisposable} from './disposable'; |
| import {type JSONCompatible, stringifyJsonWithBigints} from './json_utils'; |
| |
| export interface MemoOptions<T, K extends JSONCompatible<K>> { |
| /** |
| * JSON-serializable key identifying this computation. |
| * Changes to the key (compared via stringifyJsonWithBigints) |
| * trigger a recompute. |
| */ |
| readonly key: K; |
| /** |
| * Synchronous function that computes the value. |
| * Called only when the key changes. |
| */ |
| readonly compute: () => T; |
| } |
| |
| interface Cache<T> { |
| readonly keyStr: string; |
| readonly value: T; |
| } |
| |
| /** |
| * A single-entry synchronous memo cache. |
| * |
| * Created once per computed value on a component. Calls to `use()` with |
| * the same key return the cached result; key changes trigger a recompute. |
| * The memo owns cached `Disposable` values and disposes them when evicted. |
| */ |
| export class Memo<T> implements Disposable { |
| private cache?: Cache<T>; |
| |
| use<K extends JSONCompatible<K>>(options: MemoOptions<T, K>): T { |
| const {key, compute} = options; |
| const keyStr = stringifyJsonWithBigints(key); |
| |
| if (this.cache !== undefined && this.cache.keyStr === keyStr) { |
| return this.cache.value; |
| } |
| |
| this.disposeCache(); |
| const value = compute(); |
| this.cache = {keyStr, value}; |
| return value; |
| } |
| |
| /** |
| * Clear and dispose the cached value, forcing the next use() to recompute. |
| */ |
| invalidate(): void { |
| this.disposeCache(); |
| } |
| |
| /** Dispose the cached value. */ |
| dispose(): void { |
| this[Symbol.dispose](); |
| } |
| |
| [Symbol.dispose](): void { |
| this.disposeCache(); |
| } |
| |
| private disposeCache(): void { |
| const cache = this.cache; |
| this.cache = undefined; |
| if (cache !== undefined && isDisposable(cache.value)) { |
| cache.value[Symbol.dispose](); |
| } |
| } |
| } |