The Polarity server provides a hierarchical caching system with three scopes: global, integration, and user. The cache object is passed to your integration via the server context. This guide explains how to use the cache interfaces effectively in your integrations.
The caching system is organized in three levels, from most general to most specific:
interface PolarityCache {
global: GlobalCache; // System-wide shared cache
integration: IntegrationCache; // Integration-specific cache
user: UserCache; // User-specific cache within integration
}
import { createCacheKey } from 'polarity-integration-utils';
import type { Entity, IntegrationContext } from '@polarityio/integration-types';
interface LookupResult {
title: string;
description: string;
}
async function doLookup(entity: Entity, context: IntegrationContext): Promise<LookupResult> {
const cache = context?.cache;
if (!cache) return fetchFreshData(entity);
try {
const cacheKey = createCacheKey('lookup', entity.value);
const cached = await cache.integration.get<LookupResult>(cacheKey);
if (cached) return cached;
const result = await fetchFreshData(entity);
await cache.integration.set(cacheKey, result, { ttl: 300 }); // 5 minutes
return result;
} catch (error) {
logger.error({ err: error }, 'Cache error');
return fetchFreshData(entity); // Graceful fallback
}
}
import type { IntegrationContext } from '@polarityio/integration-types';
interface UserPreferences {
theme: string;
pageSize: number;
}
const DEFAULT_PREFERENCES: UserPreferences = { theme: 'dark', pageSize: 25 };
async function getUserPreferences(context: IntegrationContext): Promise<UserPreferences> {
const cache = context?.cache;
if (!cache) return DEFAULT_PREFERENCES;
try {
const prefs = await cache.user.get<UserPreferences>('ui_preferences');
return prefs ?? DEFAULT_PREFERENCES;
} catch (error) {
return DEFAULT_PREFERENCES;
}
}
async function updateUserPreferences(
preferences: UserPreferences,
context: IntegrationContext
): Promise<void> {
const cache = context?.cache;
if (!cache) return;
try {
await cache.user.set('ui_preferences', preferences, { ttl: 86400 }); // 24 hours
} catch (error) {
logger.error({ err: error }, 'Failed to save preferences');
}
}
import type { IntegrationContext } from '@polarityio/integration-types';
async function trackGlobalUsage(context: IntegrationContext): Promise<void> {
const cache = context?.cache;
if (!cache) return;
try {
const current = (await cache.global.get<number>('total_lookups')) ?? 0;
await cache.global.set('total_lookups', current + 1, { ttl: 86400 });
} catch (error) {
logger.warn({ err: error }, 'Failed to update global stats');
}
}
Use this pattern to check caches from most specific to most general:
import { createCacheKey } from 'polarity-integration-utils';
import type { Entity, IntegrationContext } from '@polarityio/integration-types';
interface CachedLookup {
source: 'user_cache' | 'integration_cache' | 'global_cache' | 'fresh' | 'fallback';
data: unknown;
}
async function getLookupData(entity: Entity, context: IntegrationContext): Promise<CachedLookup> {
const cache = context?.cache;
if (!cache) return { source: 'fallback', data: await fetchFreshData(entity) };
try {
const key = createCacheKey('lookup', entity.value);
// 1. Check user-specific cache first
let result = await cache.user.get(key);
if (result) return { source: 'user_cache', data: result };
// 2. Check integration cache
result = await cache.integration.get(key);
if (result) return { source: 'integration_cache', data: result };
// 3. Check global cache for known entities
result = await cache.global.get(createCacheKey('known_entity', entity.value));
if (result) return { source: 'global_cache', data: result };
// 4. Fetch fresh data and cache in integration scope for all users
const freshData = await fetchFreshData(entity);
await cache.integration.set(key, freshData, { ttl: 3600 });
return { source: 'fresh', data: freshData };
} catch (error) {
logger.error({ err: error }, 'Cache error');
return { source: 'fallback', data: await fetchFreshData(entity) };
}
}
All cache keys (get, set, and delete) must satisfy the following constraints:
/^[a-zA-Z0-9._-]+$/An Error is thrown if the key is empty, exceeds 250 characters, or contains invalid characters (e.g., spaces, colons, or slashes).
// Valid keys
'lookup_192.168.1.1'
'config.api-endpoint'
'rate-limit_counter_2024-01-15'
// Invalid keys — will throw an Error
'lookup result' // contains a space
'config:api_endpoint' // contains a colon
'cache/key/path' // contains slashes
'' // empty string
Avoid using sensitive data such as usernames or passwords directly as cache keys. Instead, use the createCacheKey utility to hash the values and produce a unique, safe key. This is especially useful when caching API tokens keyed by a user's credentials.
You should also use createCacheKey for entity values and other dynamic data that may contain characters outside the allowed set (e.g., IPv6 addresses contain colons, URLs contain slashes).
import { createCacheKey } from 'polarity-integration-utils';
import type { IntegrationContext } from '@polarityio/integration-types';
interface AuthToken {
token: string;
expiresAt: number;
}
async function getApiToken(
username: string,
password: string,
context: IntegrationContext
): Promise<AuthToken> {
const cache = context?.cache;
// Generate a key from credentials without exposing them
// e.g., "auth-token_a1b2c3d4e5..."
const cacheKey = createCacheKey('auth-token', username, password);
if (cache) {
const cached = await cache.integration.get<AuthToken>(cacheKey);
if (cached && cached.expiresAt > Date.now()) return cached;
}
const token = await authenticateWithApi(username, password);
if (cache) {
await cache.integration.set(cacheKey, token, { ttl: 3600 });
}
return token;
}
createCacheKey JSON-serializes the input values, hashes them with SHA-256, and prepends the given prefix. The returned key has the form prefix_<64-char hex digest>, so its total length is prefix.length + 1 + 64. A LibraryUsageError is thrown if the prefix contains invalid characters, or if the prefix is longer than 185 characters (which would cause the key to exceed the 250-character limit).
All cache operations support optional configuration:
interface CacheOptions {
ttl?: number; // Time-to-live in seconds
}
// Examples
await cache.global.set('key', 'value'); // No expiration
await cache.global.set('key', 'value', { ttl: 300 }); // 5 minutes
await cache.global.set('key', 'value', { ttl: 86400 }); // 24 hours
try {
const cached = await cache.integration.get<MyData>(key);
return cached ?? (await fetchFreshData());
} catch (error) {
logger.error({ err: error }, 'Cache error');
return await fetchFreshData(); // Always provide fallback
}
Keys must be 1–250 characters using only letters, digits, dots, underscores, and hyphens (/^[a-zA-Z0-9._-]+$/).
// Good — descriptive, valid characters, and unlikely to conflict
'config_api_endpoints';
'lookup_192.168.1.1';
'user_preferences_dashboard';
'rate_limit_counter_2024-01-15';
// Bad — vague and likely to conflict
'config';
'data';
'temp';
'result';
// Short-lived data (5–15 minutes)
{ ttl: 300 } // API responses that change frequently
{ ttl: 900 } // Rate limiting counters
// Medium-lived data (1–6 hours)
{ ttl: 3600 } // Lookup results
{ ttl: 21600 } // Configuration data
// Long-lived data (24+ hours)
{ ttl: 86400 } // User preferences
{ ttl: 604800 } // Weekly statistics
// Use consistent prefixes to organize keys (use dots, underscores, or hyphens — not colons)
await cache.integration.set('config.api_endpoint', endpoint);
await cache.integration.set('config.timeout', timeout);
await cache.integration.set('stats.daily_lookups', count);
await cache.integration.set('temp.processing_batch_001', batch);
const cached = await cache.integration.get<MyData>(key);
const data = cached ?? getDefaultValue(); // Always provide fallback
// Or use a typed default for objects
interface AppConfig {
timeout: number;
retries: number;
}
const config: AppConfig = (await cache.integration.get<AppConfig>('app_config')) ?? {
timeout: 30000,
retries: 3
};
Cache operations can fail for various reasons (network issues, storage limits, etc.). Always implement proper error handling:
import type { IntegrationContext } from '@polarityio/integration-types';
async function robustCacheOperation(context: IntegrationContext): Promise<unknown> {
const cache = context?.cache;
// Graceful degradation if no cache available
if (!cache) {
return await fallbackOperation();
}
try {
const result = await cache.integration.get('key');
if (result) return result;
const freshData = await fetchData();
// Don't fail the operation if caching fails
try {
await cache.integration.set('key', freshData, { ttl: 300 });
} catch (cacheError) {
logger.warn({ err: cacheError }, 'Failed to cache result');
}
return freshData;
} catch (error) {
logger.error({ err: error }, 'Cache operation failed');
return await fallbackOperation();
}
}
The cache interfaces provide a powerful way to improve integration performance while maintaining data consistency and user experience.