-
Notifications
You must be signed in to change notification settings - Fork 2
Utils Memoize
Function and method memoization with TTL and async support.
The memoize utility provides advanced caching for functions and class methods:
- TTL Support: Time-based cache expiration
- Async Functions: Proper promise handling and deduplication
-
Method Decorator:
@Memoizefor class methods - Lazy Eviction: An expired entry is dropped the next time its exact argument set is called again — there is no background sweep (see "Unbounded Cache Growth" below)
- Type Safety: Full TypeScript support
deno add @tundralibs/utilsMemoizes a function with optional time-to-live.
Parameters:
-
fn: Function to memoize -
timeout: Cache lifetime in seconds (default: 1800 — 30 minutes)
Returns: Memoized version of the function
Decorator for memoizing class methods and getters. Exported from both
@tundralibs/utils (the main barrel) and the narrower
@tundralibs/utils/memoize subpath.
import { memoize } from '@tundralibs/utils';
const expensiveCalc = (n: number): number => {
console.log('Computing...');
return n * n * n;
};
const memoized = memoize(expensiveCalc);
console.log(memoized(5)); // Logs "Computing...", returns 125
console.log(memoized(5)); // Returns 125 (cached, no log)
console.log(memoized(6)); // Logs "Computing...", returns 216import { memoize } from '@tundralibs/utils';
declare function fetchFromAPI(id: string): Promise<string>;
// Cache expires after 5 seconds
const getData = memoize(
async (id: string) => await fetchFromAPI(id),
5, // seconds
);
await getData('user123'); // Fetches from API
await getData('user123'); // Returns cached (within 5s)
// Wait 6 seconds
await new Promise((r) => setTimeout(r, 6000));
await getData('user123'); // Fetches again (cache expired)import { Memoize } from '@tundralibs/utils'; // also available at '@tundralibs/utils/memoize'
interface Data {
id: number;
}
declare const api: { get(path: string): Promise<Data> };
declare function complexCalculation(precision: number): number;
class Calculator {
@Memoize(10) // 10 second cache
async fetchData(id: number): Promise<Data> {
console.log('Fetching data...');
return await api.get(`/data/${id}`);
}
@Memoize() // 30-minute cache (default)
calculatePi(precision: number): number {
console.log('Computing π...');
return complexCalculation(precision);
}
}
const calc = new Calculator();
// First call - computes
await calc.fetchData(1);
// Second call - cached
await calc.fetchData(1);import { memoize } from '@tundralibs/utils';
declare const database: {
users: { findById(id: string): Promise<{ id: string }> };
};
const fetchUser = memoize(async (id: string) => {
return await database.users.findById(id);
});
// These run concurrently but only one DB query executes
const [user1, user2, user3] = await Promise.all([
fetchUser('123'),
fetchUser('123'),
fetchUser('123'),
]);
console.log(user1 === user2 && user2 === user3); // true (same cached result)import { memoize } from '@tundralibs/utils';
const fibonacci: (n: number) => number = memoize((n: number): number => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(40)); // Fast with memoization
// Without memoization, this would take foreverimport { memoize } from '@tundralibs/utils';
// Cache API responses for 1 minute
const getWeather = memoize(
async (city: string) => {
return await fetch(`/api/weather?city=${city}`).then((r) => r.json());
},
60, // seconds
);
// Multiple requests within 1 minute return cached data
await getWeather('London'); // API call
await getWeather('London'); // Cached
await getWeather('London'); // Cached- Choose Appropriate TTL: Balance freshness vs performance
- Pure Functions: Memoize pure functions (same input = same output)
- Argument Serialization: Simple arguments work best
- Memory Considerations: Long TTLs increase memory usage
- Time Complexity: O(1) cache lookup
- Space Complexity: O(n) where n is unique argument combinations
- Memory Cleanup: LAZY, not automatic — an entry is only dropped the next time its exact argument set is looked up again and found expired. See "Unbounded Cache Growth" below.
There is no background timer sweeping expired entries and no maximum
cache size — the internal Map only shrinks when a key is looked up
again after its TTL has passed. A memoized function called with a
large or unbounded number of distinct argument combinations (e.g.
one argument being a request ID or a timestamp) keeps every entry
alive until either that exact key is called again post-expiry, or the
whole memoized wrapper itself is garbage-collected.
import { memoize } from '@tundralibs/utils';
declare function lookupUser(id: string): { id: string };
// ❌ Every distinct id gets its own cache entry, forever — none are
// ever evicted unless that SAME id is looked up again after its TTL.
const bad = memoize((id: string) => lookupUser(id), 60);
// ✅ Memoize only over a small, bounded key space (or don't memoize
// per-request-scoped lookups at all).
const good = memoize((tier: 'free' | 'paid') => lookupUser(tier), 60);import { memoize } from '@tundralibs/utils';
declare function fetchData(id: number): Promise<string>;
// ❌ Functions as arguments don't memoize well
const bad = memoize((fn: () => unknown) => fn());
// ✅ Use primitive arguments
const good = memoize((id: number) => fetchData(id));import { memoize } from '@tundralibs/utils';
// ❌ Don't memoize functions with side effects
const bad = memoize(() => {
console.log('Side effect!');
return Math.random();
});
// ✅ Memoize pure computations
const good = memoize((n: number) => n * n);