-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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-agefrom server - Missing: Respect
Cache-Control: no-cachedirective - 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 listWith 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 dataContext
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 = {}): stringImplication: 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) - ❌
Expiresheader (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-Controlheaders with appropriatemax-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=60Current 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-updatedevent (invalidate specific resource) - Handle
resource-deletedevent (invalidate resource + collections) - Handle
collection-changedevent (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-Controlheader from responses - Respect
max-agedirective (override default TTL) - Respect
stale-while-revalidatedirective - Respect
no-cachedirective (always revalidate) - Respect
no-storedirective (don't cache) - Respect
must-revalidatedirective (no stale serving) - Parse
Expiresheader (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
InvalidationOptionsto 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)
InvalidationStrategyinterfaceStaleWhileRevalidateStrategyclassBackgroundRefreshStrategyclassAdaptiveTTLStrategyclassDependencyTrackerclassInvalidationOptionsinterface
New File: src/ogc-api/csapi/cache-events.ts (~300-400 lines)
CacheEventSubscriberinterfaceWebSocketSubscriberclassSSESubscriberclassBroadcastChannelSubscriberclassInvalidationEventinterface
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
InvalidationOptionsin 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:
- Work Item Add test coverage for GeoJSON collection validators (currently 0%) #39 (HTTP ETag Cache) - ✅ Must be implemented first
- Work Item Add test coverage for uncovered SWE Common validation paths #40 (ParseResult Cache) - ✅ Must be implemented first
- Issue Validate: CSAPI Navigator Implementation (navigator.ts) #13 (Navigator) - ✅ Complete (provides relationship context)
- Issue Validate: SensorML Validation System (validation/sensorml-validator.ts) #16 (TypedCSAPINavigator) - ✅ Complete (provides fetch infrastructure)
Blocks:
- None (this is an enhancement to existing caching)
Critical Path:
- Cannot start 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
- This is the final piece of the caching infrastructure
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-Controlheaders - 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:
- Test interaction between strategies
- Test with real cache managers (work items Add test coverage for GeoJSON collection validators (currently 0%) #39, Add test coverage for uncovered SWE Common validation paths #40)
- Test multi-tab scenarios with BroadcastChannel
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:
- 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
- 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
- Significant Effort: 34-44 hours of implementation + testing
- Server Dependency: Event-based and server-driven strategies require server support
- Complexity: Multiple strategies increase code complexity and maintenance burden
Why Still Important:
- User Experience: Stale-while-revalidate eliminates loading states
- Performance: Background refresh prevents cache expiration gaps
- Consistency: Event-based invalidation ensures data freshness across tabs/users
- Best Practices: Cache-Control support aligns with HTTP standards
- Production Quality: Advanced invalidation strategies expected in enterprise applications
Impact if Not Addressed:
⚠️ Basic caching works but less sophisticated⚠️ Users may see loading states on cache expiration⚠️ No server control over cache lifetime⚠️ No automatic invalidation on server-side changes⚠️ No multi-tab coordination- ✅ Basic caching still functional (work items Add test coverage for GeoJSON collection validators (currently 0%) #39, Add test coverage for uncovered SWE Common validation paths #40)
When to Prioritize Higher:
- After 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 and stable
- For enterprise applications requiring advanced caching
- When server provides WebSocket/SSE invalidation events
- When multi-tab coordination is critical
- When users complain about stale data or loading delays
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:
- High ROI for applications with frequent updates and multiple tabs
- Moderate ROI for typical usage patterns
- Low ROI if server doesn't support advanced features (events, Cache-Control)
- Best ROI when combined with work items Add test coverage for GeoJSON collection validators (currently 0%) #39 and Add test coverage for uncovered SWE Common validation paths #40
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.