Skip to content

Add cache invalidation strategies (time-based, event-based) #64

@Sam-Bolling

Description

@Sam-Bolling

Problem

The ogc-client-CSAPI implementation will have HTTP ETag caching (work item #39) and ParseResult caching (work item #40), but lacks sophisticated cache invalidation strategies beyond manual clearing and automatic invalidation on mutations.

Current Planned Invalidation (from work items #39 and #40):

  • Manual invalidation: clearCache(), clearCacheForResourceType()
  • Automatic invalidation on mutations: Clear cache after create/update/delete
  • Time-based invalidation: No scheduled cache refresh
  • Event-based invalidation: No external event triggers
  • Dependency-based invalidation: No relationship tracking
  • Conditional invalidation: No server-driven cache control

Missing Strategies:

1. Time-Based Invalidation (TTL with Stale-While-Revalidate)

  • Current: Simple TTL (cache entry expires, then refetch)
  • Missing: Stale-while-revalidate (return stale data while fetching fresh)
  • Missing: Background refresh before expiration
  • Missing: Different TTLs per resource type
  • Missing: Adaptive TTL based on update frequency

2. Event-Based Invalidation

  • Missing: WebSocket notifications from server
  • Missing: Server-Sent Events (SSE) for cache invalidation
  • Missing: Pub/Sub pattern for distributed invalidation
  • Missing: External event triggers (e.g., user actions)

3. Dependency-Based Invalidation

  • Missing: Invalidate related resources (e.g., delete System → invalidate its Deployments)
  • Missing: Invalidate parent when child changes
  • Missing: Invalidate collections when items change
  • Missing: Relationship graph tracking

4. Server-Driven Invalidation (Cache-Control Headers)

  • Missing: Respect Cache-Control: max-age from server
  • Missing: Respect Cache-Control: no-cache directive
  • Missing: Respect Cache-Control: must-revalidate
  • Missing: Parse and honor server cache preferences

Real-World Impact:

  • Stale Data: Users see outdated data after TTL expires but before next fetch
  • Inefficient Refresh: Full cache expiration instead of targeted invalidation
  • Poor UX: Loading states when stale data could be shown immediately
  • Server Load: No background refresh means thundering herd on expiration
  • Wasted Bandwidth: No server guidance on cache lifetime
  • Inconsistent State: Related resources out of sync

Example Scenario (Current Behavior):

// At T=0: Cache System (TTL: 5 minutes)
const system = await nav.getSystem('sensor-123');

// At T=4:59: Cache still valid
const system2 = await nav.getSystem('sensor-123'); // Instant

// At T=5:01: Cache expired
const system3 = await nav.getSystem('sensor-123'); // Full refetch, user waits

// At T=5:02: User deletes a Deployment of this System on another tab
// Cache still shows old Deployment in System's deployment list

With Advanced Invalidation (Desired Behavior):

// At T=0: Cache System (TTL: 5 minutes)
const system = await nav.getSystem('sensor-123');

// At T=4:30: Background refresh starts (before expiration)
// User gets instant response with cached data

// At T=4:35: Background refresh completes
// Cache updated transparently

// At T=5:01: Cache still fresh (was refreshed at T=4:35)
const system2 = await nav.getSystem('sensor-123'); // Instant

// At T=5:02: User deletes Deployment on another tab
// Event-based invalidation: System cache invalidated
// Related Deployment collection cache invalidated
const system3 = await nav.getSystem('sensor-123'); // Fetches fresh data

Context

This issue was identified during the comprehensive validation conducted January 27-28, 2026.

Related Validation Issues: #13 (CSAPI Navigator Implementation), #16 (TypedCSAPINavigator)

Work Item ID: 41 from Remaining Work Items

Repository: https://github.com/OS4CSAPI/ogc-client-CSAPI

Validated Commit: a71706b9592cad7a5ad06e6cf8ddc41fa5387732

Detailed Findings

1. Current Cache Infrastructure (Work Items #39 and #40)

From Work Item #39 (HTTP ETag Cache):

  • Simple TTL-based expiration (default: 5 minutes)
  • LRU eviction on max size
  • Manual invalidation methods
  • Automatic invalidation on mutations

From Work Item #40 (ParseResult Cache):

  • Simple TTL-based expiration (default: 2 minutes)
  • LRU eviction on max size and max memory
  • Manual invalidation methods
  • Automatic invalidation on mutations

What Works Well:

  • ✅ Basic caching infrastructure exists
  • ✅ Manual cache control
  • ✅ Automatic invalidation on CRUD operations

What's Missing:

  • ❌ Sophisticated invalidation strategies
  • ❌ Stale-while-revalidate pattern
  • ❌ Background refresh
  • ❌ Event-based invalidation
  • ❌ Dependency tracking
  • ❌ Server-driven cache control

2. Navigator Provides Resource Relationship Context (Issue #13)

From Issue #13 Validation Report:

File: src/ogc-api/csapi/navigator.ts (2,091 lines, 274 tests)

Resource Relationships:

  • System → Subsystems, Sampling Features, Datastreams, Control Streams, Deployments, Procedures, System Events
  • Deployment → Subdeployments, Systems
  • Datastream → Observations
  • Control Stream → Commands

Key Finding: Navigator understands resource relationships, providing foundation for dependency-based invalidation.

Evidence:

// Relationship methods that should trigger invalidation
getSystemDatastreamsUrl(systemId: string, options: DatastreamsQueryOptions = {}): string
getSystemDeploymentsUrl(systemId: string, options: DeploymentsQueryOptions = {}): string
getSystemProceduresUrl(systemId: string, options: ProceduresQueryOptions = {}): string
getDeploymentSystemsUrl(deploymentId: string, options: SystemsQueryOptions = {}): string
getDatastreamObservationsUrl(datastreamId: string, options: ObservationsQueryOptions = {}): string
getControlStreamCommandsUrl(controlStreamId: string, options: CommandsQueryOptions = {}): string

Implication: When a System is deleted, its related Datastreams, Deployments, and Procedures should be invalidated from cache.


3. TypedCSAPINavigator Fetches Resources (Issue #16)

From Issue #16 Validation Report:

File: src/ogc-api/csapi/typed-navigator.ts (11,366 bytes, ~320 lines, 26 tests)

TypedCSAPINavigator provides high-level API with automatic fetching and parsing.

Current _fetch() Method:

private async _fetch(
  url: string,
  options: TypedFetchOptions = {}
): Promise<Response> {
  const fetchFn = options.fetch || fetch;
  const headers: Record<string, string> = {
    ...options.headers,
  };
  
  // Accept header negotiation
  // ...
  
  const response = await fetchFn(url, { headers });
  
  if (!response.ok) {
    throw new Error(
      `HTTP ${response.status}: ${response.statusText} (${url})`
    );
  }

  return response;
}

Key Finding: Response headers available but not used for cache control.

Missing Response Header Handling:

  • Cache-Control: max-age=3600 (server specifies cache lifetime)
  • Cache-Control: no-cache (server requires revalidation)
  • Cache-Control: no-store (server forbids caching)
  • Cache-Control: must-revalidate (server requires fresh data after expiration)
  • Expires header (legacy cache expiration)

Implication: Server cannot control client cache behavior, leading to stale or overly fresh data.


4. Invalidation Patterns from OGC API Best Practices

OGC API - Features Caching Recommendations:

  • Servers SHOULD provide Cache-Control headers with appropriate max-age
  • Clients SHOULD respect server cache directives
  • Stable resources (Systems, Deployments) can have longer cache times (hours)
  • Dynamic resources (Observations) should have short cache times (seconds/minutes)

Common Cache-Control Patterns:

# Stable System (cache for 1 hour)
Cache-Control: max-age=3600

# Moderately stable Datastream (cache for 5 minutes)
Cache-Control: max-age=300

# Real-time Observation (don't cache)
Cache-Control: no-cache, no-store, must-revalidate

# Stale-while-revalidate (return stale up to 60s while refreshing)
Cache-Control: max-age=300, stale-while-revalidate=60

Current Implementation: None of these directives are honored.


5. Performance Impact of Invalidation Strategies

Scenario: Dashboard with 10 Systems, refreshing every 5 seconds

Without Advanced Invalidation (Current):

T=0:00 - Fetch 10 Systems (cache for 2 minutes)
T=0:05 - Return cached Systems (instant)
T=0:10 - Return cached Systems (instant)
...
T=2:00 - Cache expired, fetch 10 Systems (user waits)
T=2:05 - Return cached Systems (instant)
...

Issues:
- User waits at T=2:00, T=4:00, T=6:00 (every 2 minutes)
- Thundering herd: All 10 Systems refetch simultaneously
- No stale data shown during refresh

With Stale-While-Revalidate:

T=0:00 - Fetch 10 Systems (cache for 2 minutes, stale-revalidate for 30s)
T=0:05 - Return cached Systems (instant)
T=0:10 - Return cached Systems (instant)
...
T=2:00 - Cache expired but within stale window
         → Return stale cached Systems (instant)
         → Background fetch starts
T=2:02 - Background fetch completes, cache updated
T=2:05 - Return fresh cached Systems (instant)

Benefits:
- User NEVER waits (stale data returned immediately)
- Background refresh transparent
- Better perceived performance

With Background Refresh (Before Expiration):

T=0:00 - Fetch 10 Systems (cache for 2 minutes)
T=0:05 - Return cached Systems (instant)
...
T=1:50 - Background refresh starts (10 seconds before expiration)
T=1:52 - Background refresh completes, cache updated
T=2:00 - Return fresh cached Systems (instant, never expired)

Benefits:
- Cache never expires (always refreshed before expiration)
- User never sees loading state
- Smooth, uninterrupted experience

With Event-Based Invalidation:

T=0:00 - Fetch 10 Systems, subscribe to WebSocket updates
T=0:05 - Return cached Systems (instant)
T=1:00 - Server sends event: System 'sensor-123' updated
         → Invalidate 'sensor-123' cache
         → Background fetch 'sensor-123'
T=1:02 - Cache updated with fresh 'sensor-123'
T=1:05 - Return cached Systems (9 stale, 1 fresh)

Benefits:
- Only changed resources refetched
- Near-instant updates (server-pushed)
- Minimal bandwidth usage

Proposed Solution

1. Time-Based Invalidation Strategies

A. Stale-While-Revalidate Pattern

Implement RFC 5861 stale-while-revalidate:

interface CacheEntry {
  url: string;
  data: any;
  etag?: string;
  timestamp: number;
  ttl: number;                    // Normal cache lifetime
  staleWhileRevalidate?: number;  // Additional time to serve stale
}

class CacheManager {
  async get(url: string): Promise<CacheEntry | undefined> {
    const entry = this.cache.get(url);
    if (!entry) return undefined;
    
    const age = Date.now() - entry.timestamp;
    
    // Fresh: Within TTL
    if (age < entry.ttl) {
      return entry;
    }
    
    // Stale but revalidate: Within stale-while-revalidate window
    if (entry.staleWhileRevalidate && age < entry.ttl + entry.staleWhileRevalidate) {
      // Return stale data immediately
      const staleEntry = { ...entry };
      
      // Start background revalidation
      this.revalidateInBackground(url).catch(err => {
        console.warn('Background revalidation failed:', err);
      });
      
      return staleEntry;
    }
    
    // Expired: Remove from cache
    this.cache.delete(url);
    return undefined;
  }
  
  private async revalidateInBackground(url: string): Promise<void> {
    // Fetch fresh data without blocking
    const fresh = await this.fetch(url);
    this.cache.set(url, fresh);
  }
}

B. Background Refresh (Before Expiration)

Proactively refresh cache before expiration:

interface CacheEntry {
  url: string;
  data: any;
  timestamp: number;
  ttl: number;
  refreshThreshold: number;  // Refresh when age > ttl - refreshThreshold
}

class CacheManager {
  async get(url: string): Promise<CacheEntry | undefined> {
    const entry = this.cache.get(url);
    if (!entry) return undefined;
    
    const age = Date.now() - entry.timestamp;
    
    // Check if refresh needed
    if (age > entry.ttl - entry.refreshThreshold) {
      // Start background refresh (don't wait)
      this.refreshInBackground(url).catch(err => {
        console.warn('Background refresh failed:', err);
      });
    }
    
    // Return current data (even if refresh started)
    if (age < entry.ttl) {
      return entry;
    }
    
    return undefined;
  }
  
  private async refreshInBackground(url: string): Promise<void> {
    const fresh = await this.fetch(url);
    this.cache.set(url, fresh);
  }
}

C. Adaptive TTL

Adjust TTL based on resource update frequency:

interface CacheEntry {
  url: string;
  data: any;
  timestamp: number;
  ttl: number;
  updateHistory: number[];  // Timestamps of last N updates
}

class CacheManager {
  private calculateAdaptiveTTL(entry: CacheEntry): number {
    if (entry.updateHistory.length < 2) {
      return entry.ttl; // Not enough data
    }
    
    // Calculate average time between updates
    const intervals: number[] = [];
    for (let i = 1; i < entry.updateHistory.length; i++) {
      intervals.push(entry.updateHistory[i] - entry.updateHistory[i - 1]);
    }
    const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
    
    // Set TTL to 50% of average update interval
    // (refresh before next update expected)
    const adaptiveTTL = avgInterval * 0.5;
    
    // Clamp to reasonable range (30s - 1 hour)
    return Math.max(30000, Math.min(adaptiveTTL, 3600000));
  }
}

2. Event-Based Invalidation

A. WebSocket Notifications

Server pushes cache invalidation events:

class CacheInvalidationSubscriber {
  private ws: WebSocket | null = null;
  
  constructor(
    private baseUrl: string,
    private cacheManager: CacheManager
  ) {}
  
  connect(): void {
    const wsUrl = this.baseUrl.replace('http', 'ws') + '/cache-events';
    this.ws = new WebSocket(wsUrl);
    
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleInvalidationEvent(message);
    };
  }
  
  private handleInvalidationEvent(event: InvalidationEvent): void {
    switch (event.type) {
      case 'resource-updated':
        // Invalidate specific resource
        this.cacheManager.invalidate(event.resourceUrl);
        break;
        
      case 'resource-deleted':
        // Invalidate resource and related collections
        this.cacheManager.invalidate(event.resourceUrl);
        this.cacheManager.invalidateCollections(event.resourceType);
        break;
        
      case 'collection-changed':
        // Invalidate entire collection
        this.cacheManager.invalidateResourceType(event.resourceType);
        break;
    }
  }
}

interface InvalidationEvent {
  type: 'resource-updated' | 'resource-deleted' | 'collection-changed';
  resourceUrl?: string;
  resourceType?: CSAPIResourceType;
  timestamp: number;
}

B. Server-Sent Events (SSE)

Alternative to WebSocket (simpler, HTTP-based):

class SSEInvalidationSubscriber {
  private eventSource: EventSource | null = null;
  
  connect(): void {
    const sseUrl = `${this.baseUrl}/cache-events`;
    this.eventSource = new EventSource(sseUrl);
    
    this.eventSource.addEventListener('cache-invalidate', (event) => {
      const data = JSON.parse(event.data);
      this.cacheManager.invalidate(data.url);
    });
  }
  
  disconnect(): void {
    this.eventSource?.close();
  }
}

C. Custom Event Bus

Client-side event coordination (for multi-tab scenarios):

class CacheEventBus {
  private channel: BroadcastChannel;
  
  constructor(private cacheManager: CacheManager) {
    this.channel = new BroadcastChannel('cache-events');
    
    this.channel.onmessage = (event) => {
      if (event.data.type === 'invalidate') {
        this.cacheManager.invalidate(event.data.url);
      }
    };
  }
  
  notifyInvalidation(url: string): void {
    this.channel.postMessage({
      type: 'invalidate',
      url,
      timestamp: Date.now(),
    });
  }
}

3. Dependency-Based Invalidation

Track resource relationships and invalidate cascading:

class DependencyTracker {
  private dependencies = new Map<string, Set<string>>();
  
  // Register dependency: parent depends on child
  addDependency(parentUrl: string, childUrl: string): void {
    if (!this.dependencies.has(parentUrl)) {
      this.dependencies.set(parentUrl, new Set());
    }
    this.dependencies.get(parentUrl)!.add(childUrl);
  }
  
  // Get all URLs that depend on this URL
  getDependents(url: string): Set<string> {
    const dependents = new Set<string>();
    
    for (const [parent, children] of this.dependencies.entries()) {
      if (children.has(url)) {
        dependents.add(parent);
      }
    }
    
    return dependents;
  }
  
  // Invalidate URL and all dependents
  invalidateCascading(url: string, cacheManager: CacheManager): void {
    // Invalidate the URL itself
    cacheManager.invalidate(url);
    
    // Invalidate all dependents recursively
    const dependents = this.getDependents(url);
    for (const dependent of dependents) {
      this.invalidateCascading(dependent, cacheManager);
    }
  }
}

// Usage
const tracker = new DependencyTracker();

// When fetching System Datastreams, register dependency
const systemUrl = nav.getSystemUrl('sensor-123');
const datastreamsUrl = nav.getSystemDatastreamsUrl('sensor-123');
tracker.addDependency(systemUrl, datastreamsUrl);

// When a Datastream is updated, invalidate System
const datastreamUrl = nav.getDatastreamUrl('datastream-456');
tracker.invalidateCascading(datastreamUrl, cacheManager);
// → Invalidates datastream-456
// → Invalidates sensor-123 (depends on its datastreams)

4. Server-Driven Invalidation (Cache-Control)

Respect server cache directives:

class CacheManager {
  async fetch(url: string): Promise<CacheEntry> {
    const response = await fetch(url);
    const data = await response.json();
    
    // Parse Cache-Control header
    const cacheControl = this.parseCacheControl(
      response.headers.get('Cache-Control')
    );
    
    let ttl = this.defaultTTL;
    let staleWhileRevalidate: number | undefined;
    
    if (cacheControl) {
      // Respect max-age directive
      if (cacheControl.maxAge !== undefined) {
        ttl = cacheControl.maxAge * 1000; // Convert to ms
      }
      
      // Check no-cache/no-store directives
      if (cacheControl.noCache || cacheControl.noStore) {
        ttl = 0; // Don't cache
      }
      
      // Extract stale-while-revalidate
      if (cacheControl.staleWhileRevalidate !== undefined) {
        staleWhileRevalidate = cacheControl.staleWhileRevalidate * 1000;
      }
    }
    
    return {
      url,
      data,
      timestamp: Date.now(),
      ttl,
      staleWhileRevalidate,
      etag: response.headers.get('ETag') || undefined,
    };
  }
  
  private parseCacheControl(header: string | null): CacheControlDirectives | null {
    if (!header) return null;
    
    const directives: CacheControlDirectives = {};
    
    header.split(',').forEach(directive => {
      const [key, value] = directive.trim().split('=');
      
      switch (key) {
        case 'max-age':
          directives.maxAge = parseInt(value);
          break;
        case 'stale-while-revalidate':
          directives.staleWhileRevalidate = parseInt(value);
          break;
        case 'no-cache':
          directives.noCache = true;
          break;
        case 'no-store':
          directives.noStore = true;
          break;
        case 'must-revalidate':
          directives.mustRevalidate = true;
          break;
      }
    });
    
    return directives;
  }
}

interface CacheControlDirectives {
  maxAge?: number;
  staleWhileRevalidate?: number;
  noCache?: boolean;
  noStore?: boolean;
  mustRevalidate?: boolean;
}

5. Configuration

Invalidation Strategy Options:

interface InvalidationOptions {
  // Time-based
  enableStaleWhileRevalidate?: boolean;  // Default: true
  staleWhileRevalidateDuration?: number; // Default: 60s
  enableBackgroundRefresh?: boolean;     // Default: false
  backgroundRefreshThreshold?: number;   // Default: 10s before expiration
  enableAdaptiveTTL?: boolean;          // Default: false
  
  // Event-based
  enableWebSocketInvalidation?: boolean; // Default: false
  webSocketUrl?: string;
  enableSSEInvalidation?: boolean;       // Default: false
  sseUrl?: string;
  enableBroadcastChannel?: boolean;      // Default: false
  
  // Dependency-based
  enableDependencyTracking?: boolean;    // Default: false
  trackRelationships?: boolean;          // Default: false
  
  // Server-driven
  respectCacheControl?: boolean;         // Default: true
  overrideServerMaxAge?: boolean;        // Default: false
}

// Usage
const nav = new TypedCSAPINavigator(collection, {
  // Enable stale-while-revalidate for smooth UX
  enableStaleWhileRevalidate: true,
  staleWhileRevalidateDuration: 60000, // 60 seconds
  
  // Enable background refresh to prevent expiration
  enableBackgroundRefresh: true,
  backgroundRefreshThreshold: 10000, // 10 seconds before expiration
  
  // Respect server cache directives
  respectCacheControl: true,
});

Acceptance Criteria

Time-Based Invalidation (15 criteria)

Stale-While-Revalidate:

  • Implement stale-while-revalidate pattern (RFC 5861)
  • Return stale data immediately when within revalidate window
  • Start background revalidation when serving stale data
  • Update cache when background revalidation completes
  • Log errors for failed background revalidation (don't throw)
  • Configure stale-while-revalidate duration per resource type
  • Default stale-while-revalidate: 60 seconds

Background Refresh:

  • Implement background refresh before expiration
  • Configure refresh threshold (default: 10 seconds before expiration)
  • Start background refresh when threshold reached
  • Don't block on background refresh (return current data)
  • Update cache when background refresh completes

Adaptive TTL:

  • Track update timestamps for resources
  • Calculate average update interval
  • Adjust TTL to 50% of average interval
  • Clamp adaptive TTL to reasonable range (30s - 1 hour)

Event-Based Invalidation (12 criteria)

WebSocket Support:

  • Implement WebSocket connection for cache events
  • Subscribe to server invalidation events
  • Handle resource-updated event (invalidate specific resource)
  • Handle resource-deleted event (invalidate resource + collections)
  • Handle collection-changed event (invalidate resource type)
  • Auto-reconnect on WebSocket disconnect
  • Configure WebSocket URL

Server-Sent Events (SSE):

  • Implement SSE connection for cache events
  • Subscribe to server invalidation events
  • Handle SSE cache invalidation messages
  • Configure SSE URL

BroadcastChannel:

  • Implement BroadcastChannel for multi-tab coordination
  • Broadcast invalidation events to other tabs
  • Receive and handle invalidation events from other tabs

Dependency-Based Invalidation (8 criteria)

  • Implement DependencyTracker class
  • Track parent-child relationships between resources
  • Register dependencies when fetching related resources
  • Invalidate parent when child updated
  • Invalidate children when parent deleted
  • Invalidate collections when items change
  • Support recursive invalidation (cascading)
  • Configure dependency tracking (default: disabled)

Server-Driven Invalidation (10 criteria)

  • Parse Cache-Control header from responses
  • Respect max-age directive (override default TTL)
  • Respect stale-while-revalidate directive
  • Respect no-cache directive (always revalidate)
  • Respect no-store directive (don't cache)
  • Respect must-revalidate directive (no stale serving)
  • Parse Expires header (legacy support)
  • Configure respect for server directives (default: true)
  • Allow overriding server directives (for testing)
  • Log warning when server says no-cache but caching enabled

Configuration (6 criteria)

  • Add InvalidationOptions to cache configuration
  • Default: Stale-while-revalidate enabled (60s)
  • Default: Background refresh disabled (opt-in)
  • Default: Event-based invalidation disabled (opt-in)
  • Default: Dependency tracking disabled (opt-in)
  • Default: Respect Cache-Control enabled

Testing (25 criteria)

Stale-While-Revalidate Tests (6 tests):

  • Return fresh data when within TTL
  • Return stale data when within revalidate window
  • Start background revalidation when serving stale
  • Update cache after background revalidation
  • Return undefined when outside revalidate window
  • Handle background revalidation errors gracefully

Background Refresh Tests (4 tests):

  • Start background refresh when threshold reached
  • Update cache after background refresh
  • Don't block on background refresh
  • Handle background refresh errors

Adaptive TTL Tests (4 tests):

  • Calculate average update interval
  • Adjust TTL to 50% of interval
  • Clamp TTL to min/max range
  • Return default TTL when insufficient data

Event-Based Tests (6 tests):

  • WebSocket connection and message handling
  • SSE connection and event handling
  • BroadcastChannel multi-tab coordination
  • Handle reconnection on disconnect
  • Invalidate on server events
  • Auto-reconnect with exponential backoff

Dependency Tests (3 tests):

  • Register and track dependencies
  • Invalidate dependents recursively
  • Handle circular dependencies gracefully

Cache-Control Tests (2 tests):

  • Parse and respect Cache-Control directives
  • Override server directives when configured

Documentation (5 criteria)

  • Document stale-while-revalidate pattern in README
  • Document background refresh strategy
  • Document event-based invalidation setup
  • Document dependency tracking usage
  • Document Cache-Control header support

Implementation Notes

Files to Create

New File: src/ogc-api/csapi/cache-invalidation.ts (~400-500 lines)

  • InvalidationStrategy interface
  • StaleWhileRevalidateStrategy class
  • BackgroundRefreshStrategy class
  • AdaptiveTTLStrategy class
  • DependencyTracker class
  • InvalidationOptions interface

New File: src/ogc-api/csapi/cache-events.ts (~300-400 lines)

  • CacheEventSubscriber interface
  • WebSocketSubscriber class
  • SSESubscriber class
  • BroadcastChannelSubscriber class
  • InvalidationEvent interface

New File: src/ogc-api/csapi/cache-invalidation.spec.ts (~500-600 lines)

  • Stale-while-revalidate tests
  • Background refresh tests
  • Adaptive TTL tests
  • Event-based tests
  • Dependency tracking tests

Files to Modify

Modify: src/ogc-api/csapi/http-cache.ts (from work item #39)

  • Add stale-while-revalidate support
  • Add background refresh support
  • Add Cache-Control parsing
  • Integrate with event subscribers
  • ~100-150 lines added

Modify: src/ogc-api/csapi/parse-result-cache.ts (from work item #40)

  • Add stale-while-revalidate support
  • Add background refresh support
  • Integrate with dependency tracker
  • ~80-120 lines added

Modify: src/ogc-api/csapi/typed-navigator.ts

  • Accept InvalidationOptions in constructor
  • Initialize event subscribers (WebSocket/SSE/BroadcastChannel)
  • Register dependencies when fetching related resources
  • Parse Cache-Control headers in _fetch()
  • ~150-200 lines added

Modify: README.md

  • Add "Advanced Cache Invalidation" section
  • Document stale-while-revalidate usage
  • Document event-based invalidation setup
  • Document dependency tracking
  • Document Cache-Control support
  • ~200-250 lines added

Implementation Phases

Phase 1: Time-Based Strategies (8-10 hours)

  • Implement stale-while-revalidate
  • Implement background refresh
  • Implement adaptive TTL
  • Basic tests

Phase 2: Server-Driven Invalidation (4-6 hours)

  • Parse Cache-Control headers
  • Respect server directives
  • Tests

Phase 3: Event-Based Invalidation (10-12 hours)

  • Implement WebSocket subscriber
  • Implement SSE subscriber
  • Implement BroadcastChannel
  • Auto-reconnect logic
  • Tests

Phase 4: Dependency Tracking (6-8 hours)

  • Implement DependencyTracker
  • Register dependencies in navigator
  • Cascading invalidation
  • Tests

Phase 5: Integration and Documentation (6-8 hours)

  • Integrate all strategies with cache managers
  • Configuration options
  • Documentation
  • Integration tests

Total Estimated Effort: 34-44 hours

Dependencies

Requires:

Blocks:

  • None (this is an enhancement to existing caching)

Critical Path:

Caveats

Server Support Required:

For Event-Based Invalidation:

  • Server MUST provide WebSocket or SSE endpoint
  • Server MUST send invalidation events on resource changes
  • If server doesn't support events, gracefully degrade (no event invalidation)

For Server-Driven Invalidation:

  • Server SHOULD provide Cache-Control headers
  • If server doesn't provide headers, use client defaults

Performance Considerations:

Stale-While-Revalidate:

  • Background revalidation uses network/CPU
  • May increase server load slightly
  • Benefits outweigh costs for frequently accessed resources

Background Refresh:

  • Proactive refresh before expiration
  • Prevents cache expiration but increases background requests
  • Configure threshold carefully (default: 10s before expiration)

Event Subscriptions:

  • WebSocket/SSE connections use resources
  • One persistent connection per navigator instance
  • Consider connection pooling for multiple navigators

Dependency Tracking:

  • Memory overhead for dependency graph
  • Recursive invalidation can be expensive
  • Opt-in by default (disabled unless configured)

Browser Compatibility:

  • WebSocket: IE10+, all modern browsers
  • SSE: All modern browsers (not IE)
  • BroadcastChannel: Chrome 54+, Firefox 38+, Edge 79+ (not Safari)
  • Provide polyfills or fallbacks for older browsers

Testing Requirements

Unit Tests:

  • Mock WebSocket/SSE connections
  • Mock timer functions for TTL/refresh
  • Test each strategy independently

Integration Tests:

Performance Tests:

  • Measure stale-while-revalidate latency (<1ms expected)
  • Measure background refresh overhead (<5% expected)
  • Measure dependency invalidation performance

Manual Testing:

  • Test with live OGC CSAPI server (if supports events)
  • Test Cache-Control headers from real server
  • Test multi-tab coordination in browser
  • Monitor WebSocket/SSE connections in DevTools

Priority Justification

Priority: Low

Why Low Priority:

  1. Depends on Other Work: Cannot implement until work items Add test coverage for GeoJSON collection validators (currently 0%) #39 and Add test coverage for uncovered SWE Common validation paths #40 are complete
  2. Advanced Feature: Basic caching (work items Add test coverage for GeoJSON collection validators (currently 0%) #39, Add test coverage for uncovered SWE Common validation paths #40) sufficient for most use cases
  3. Significant Effort: 34-44 hours of implementation + testing
  4. Server Dependency: Event-based and server-driven strategies require server support
  5. Complexity: Multiple strategies increase code complexity and maintenance burden

Why Still Important:

  1. User Experience: Stale-while-revalidate eliminates loading states
  2. Performance: Background refresh prevents cache expiration gaps
  3. Consistency: Event-based invalidation ensures data freshness across tabs/users
  4. Best Practices: Cache-Control support aligns with HTTP standards
  5. Production Quality: Advanced invalidation strategies expected in enterprise applications

Impact if Not Addressed:

When to Prioritize Higher:

Effort Estimate: 34-44 hours

  • Time-based strategies: 8-10 hours
  • Server-driven invalidation: 4-6 hours
  • Event-based invalidation: 10-12 hours
  • Dependency tracking: 6-8 hours
  • Integration + documentation: 6-8 hours

ROI Analysis:

Recommendation: Implement after work items #39 and #40 are complete, stable, and proven effective. Prioritize stale-while-revalidate and Cache-Control support first (highest impact), then event-based invalidation if server supports it, and finally dependency tracking as polish.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions