polarity-integration-utils
    Preparing search index...

    Cache Interface Guide

    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
    }
    • Scope: Shared across all integrations and users
    • Use cases: Feature flags, system statistics, shared configuration
    • Key namespace: Global (use descriptive prefixes to avoid conflicts)
    • Scope: Shared among all users of a specific integration
    • Use cases: API responses, configuration, lookup results
    • Key namespace: Per integration ID
    • Scope: Specific to individual users within an integration
    • Use cases: User preferences, recent activity, personalized data
    • Key namespace: Per integration ID and user ID
    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:

    • Length: 1–250 characters
    • Allowed characters: Letters, digits, dots, underscores, and hyphens — must match the pattern /^[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();
    }
    }
    1. Cache appropriate data sizes — Avoid caching very large objects
    2. Use reasonable TTL values — Balance freshness vs performance
    3. Implement cache warming for frequently accessed data
    4. Monitor cache hit rates in production
    5. Clean up expired data with appropriate TTL values

    The cache interfaces provide a powerful way to improve integration performance while maintaining data consistency and user experience.