Advanced Usage: Caching
Guantr incorporates an optional caching layer to enhance performance, particularly when dealing with context resolution or repeated permission checks. Understanding and potentially customizing this cache can be beneficial in high-throughput applications.
How Caching Works
- Purpose: Caching aims to reduce redundant computations by storing the results of certain operations, such as resolving contextual operands or the outcomes of specific
can
/cannot
checks. - Integration: The caching mechanism is integrated as an optional part of the
Storage
interface. Any storage adapter provided tocreateGuantr
can include acache
property implementing methods for setting (set
), getting (get
), checking existence (has
), and clearing (clear
) cache entries. - Default Behavior: Guantr's default
InMemoryStorage
includes a basic, in-memory cache implementation using a simple JavaScriptMap
. This provides caching out-of-the-box without external dependencies but lacks persistence or advanced eviction strategies (like TTL or LRU).
Custom Cache Implementation
If the default in-memory cache isn't sufficient (e.g., you need persistence, shared caching via Redis/Memcached, or specific eviction policies like Time-To-Live (TTL) or Least Recently Used (LRU)), you have two main options:
- Implement a Custom Storage Adapter: Create a class that fully implements the
Storage
interface fromguantr/storage/types
, including thecache
property with your desired logic (e.g., interacting with Redis). - Extend
InMemoryStorage
: If you only need to modify the caching behavior of the default storage, you can extendInMemoryStorage
and override itscache
property.
Example: Extending InMemoryStorage with Custom Logic
Here’s how you might extend InMemoryStorage
to add simple logging to the cache methods, demonstrating the override points:
import { InMemoryStorage } from 'guantr/storage';
import type { Storage } from 'guantr/storage'; // Import the interface type
// Ensure GuantrMeta is defined if you use typed Guantr
// import type { GuantrMeta } from 'guantr';
// import { createGuantr } from 'guantr';
class LoggingCacheStorage extends InMemoryStorage {
// Override the 'cache' property defined in the Storage interface
override cache: Required<Storage['cache']> = {
// Use the parent class's underlying map for storage
// Or replace with your own Map, Redis client, etc.
async set<T>(key: string, value: T): Promise<void> {
console.log(`CACHE SET: Key="${key}"`);
// Call the original implementation to actually store the value
await super.cache.set(key, value);
},
async get<T>(key: string): Promise<T | null | undefined> {
console.log(`CACHE GET: Key="${key}"`);
// Call the original implementation to retrieve the value
const value = await super.cache.get<T>(key);
console.log(`CACHE HIT : Key="${key}"`, value !== undefined && value !== null);
return value;
},
async has(key: string): Promise<boolean> {
console.log(`CACHE HAS: Key="${key}"`);
// Call the original implementation
return super.cache.has ? await super.cache.has(key) : false; // Default InMemoryStorage might not have 'has' explicitly separate from get
},
async clear(): Promise<void> {
console.log('CACHE CLEAR: Clearing all cache entries');
// Call the original implementation
await super.cache.clear();
}
};
}
// --- Usage ---
async function initialize() {
const customStorage = new LoggingCacheStorage();
// Pass the custom storage instance during initialization
// const guantr = await createGuantr<MyMeta>({ storage: customStorage });
// Now, Guantr operations that use the cache will trigger the console logs
// await guantr.can(...); // Might trigger get/set depending on internal logic
}
initialize();
Explanation:
- We extend
InMemoryStorage
. - We use
override cache: Required<Storage['cache']>
to explicitly override thecache
property defined in theStorage
interface, ensuring we provide all required cache methods (set
,get
,clear
, and optionallyhas
). - Inside our custom methods (
set
,get
,has
,clear
), we addconsole.log
statements. - We still call
super.cache.set/get/has/clear
to leverage the base class's actual storage mechanism (theMap
). In a more complex scenario (like adding TTL), you would replace these calls with your custom storage and retrieval logic. - Finally, an instance of
LoggingCacheStorage
is passed tocreateGuantr
.
Important Considerations
- Eviction Policies (TTL, LRU): Guantr itself does not implement cache eviction logic like Time-To-Live (TTL) or Least Recently Used (LRU). If you need such policies, they must be implemented within your custom
cache
methods in your storage adapter. - Cache Invalidation: Be mindful of cache invalidation. If underlying rules or context data changes frequently, a long-lived cache might serve stale permissions. Guantr typically clears relevant cache entries when
setRules
is called, but external context changes might require manual cache clearing viastorage.cache.clear()
or more granular removal if your adapter supports it. - Interface Compliance: Ensure your custom
cache
implementation adheres to the method signatures defined in theStorage['cache']
interface.
By understanding Guantr's caching mechanism and how to customize it via the storage adapter, you can optimize permission check performance for your specific application needs.