Caching Strategies
Cachemate provides three distinct caching strategies tailored for different database querying patterns:
- Join Cache (
withJoinCache/client.cache): Multi-table relational queries invalidated via table version vectors. - Flat Cache (
withFlatCache/client.flat): Isolated namespace caching for key-value records or unversioned entities. - Rows Cache (
withRowsCache/client.rows): Two-tier row-level caching for paginated collections, feeds, and filtered search.
1. Join Cache (withJoinCache)
Join Cache is designed for SQL joins or aggregated documents that span multiple database tables.
import { withJoinCache, type JoinCacheParams, type JoinCacheContext } from 'cachemate';
Key Derivation Algorithm
Key Format: [keyPrefix]:[sorted-table-tags]:[composite-versions]:[filters-hash]
Example: cm:o-u:12:4:b7a9f0c2e3d1
- Table Deduplication & Sorting: Tables in
tables: ['users', 'orders']are mapped to tags (u,o), deduplicated, and sorted alphabetically (o-u). - Version Retrieval: Versions for each table (
cm:ver:orders=12,cm:ver:users=4) are retrieved viamget(). - Filter Hashing: The
filtersparameter is converted to a deterministic string viagetDeterministicString()(sorting object keys recursively) and hashed using SHA-1 (truncated to 12 hex characters).
Execution Flow
client.cache(params)
│
▼
buildJoinKey() ──── (Unregistered table error) ────► [ Fallback to loader() ]
│
▼
readJsonCache()
│
┌────┴── ──────────────────────┐
│ Hit │ Miss
▼ ▼
Return parsed JSON Execute loader()
│
▼
writeJsonCache(key, data, ttl)
│
▼
Return fresh data
2. Flat Cache (withFlatCache)
Flat Cache is designed for queries that do not require relational table invalidation, such as third-party API responses, computed tokens, or static configuration.
import { withFlatCache, type FlatCacheParams, type FlatCacheContext } from 'cachemate';
Key Derivation Algorithm
Key Format: [keyPrefix]:[namespace]:[filters-hash]
Example: cm:exchange-rates:8f2a1b9c3e4d
Signature
export interface FlatCacheParams<T> {
ns: string;
filters: unknown;
ttlSeconds?: number;
loader: () => Promise<T>;
}
3. Rows Cache (withRowsCache)
For high-throughput lists (e.g. product catalogs, search results, timeline feeds), caching the entire list as a single JSON blob leads to severe duplication across pages.
Rows Cache implements a two-tier cache:
- Index Cache (List Key): Caches only the ordered array of row IDs (e.g.
['prod_1', 'prod_2', 'prod_3']) and optional querymeta(pagination counts). - Row Entities (Entity Keys): Caches each individual row entity by its ID (e.g.
cm:row:products:prod_1).
Cache Hit Optimization
- When the list key hits, Cachemate issues an
mget()for all entity IDs in a single round trip. - If all entity rows are present in cache, the response is reassembled instantly.
- If any individual row is missing (e.g. partially evicted), Cachemate fetches only the missing IDs via
getAllById(missingIds).
Non-Blocking Background Write
When a cache miss occurs on getByAll():
- Fresh results are returned immediately to the API caller.
- Cache population runs asynchronously in the background (
void (async () => { ... })()), auto-pipeliningpool.set()commands to avoid blocking the HTTP response.
Signature
export type RowsParams<T, M = unknown> = {
ttlSeconds?: number;
rowId: (row: T) => string;
getByAll: () => Promise<{ rows: T[]; meta?: M } | T[]>;
getAllById: (ids: string[]) => Promise<T[]>;
} & (
| { namespace: string; tables: string[]; filters: unknown; listKey?: never; rowKey?: never }
| { listKey: string; rowKey: (id: string) => string; namespace?: never; tables?: never; filters?: never }
);