Skip to content

Implement client-side caching of frequently accessed resources #63

@Sam-Bolling

Description

@Sam-Bolling

Problem

The ogc-client-CSAPI implementation has no client-side caching of parsed resources, resulting in redundant parsing operations and memory inefficiency when accessing the same resources multiple times.

Current State:

  • TypedCSAPINavigator fetches and re-parses resources on every call
  • No caching of parsed ParseResult<T> objects
  • No memory of previously accessed resources
  • Duplicate parsing work for frequently accessed resources
  • No sharing of parsed data across method calls

Real-World Impact:

  • CPU Waste: Parsing the same 10KB System feature 100 times wastes CPU cycles
  • Memory Inefficiency: Multiple parsed copies of the same resource in memory
  • Latency: Unnecessary JSON parsing delays response time (1-5ms per parse)
  • User Experience: Slower application responsiveness for repeated queries
  • Battery Drain: Mobile devices waste battery on redundant parsing

Example Scenario:

// Current behavior: Parse every time
const nav = new TypedCSAPINavigator(collection);

// Parse System (2ms)
const system1 = await nav.getSystem('sensor-123');

// Parse SAME System again (2ms wasted)
const system2 = await nav.getSystem('sensor-123');

// Parse SAME System again (2ms wasted)
const system3 = await nav.getSystem('sensor-123');

// Total parsing time: 6ms (4ms wasted on duplicate parsing)
// Memory: 3 copies of the same parsed object

With Client-Side Caching (Desired Behavior):

// First access: Parse once (2ms)
const system1 = await nav.getSystem('sensor-123');

// Second access: Return cached (0ms)
const system2 = await nav.getSystem('sensor-123');

// Third access: Return cached (0ms)
const system3 = await nav.getSystem('sensor-123');

// Total parsing time: 2ms (67% faster)
// Memory: 1 shared parsed object

Difference from Work Item #39 (ETags):

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: 40 from Remaining Work Items

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

Validated Commit: a71706b9592cad7a5ad06e6cf8ddc41fa5387732

Detailed Findings

1. TypedCSAPINavigator Parses on Every Call (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 parsing for all 7 CSAPI resource types (Systems, Deployments, Procedures, Sampling Features, Properties, Datastreams, Control Streams).

Current Implementation Pattern:

async getSystem(
  systemId: string,
  options: TypedFetchOptions = {}
): Promise<ParseResult<SystemFeature>> {
  const url = this.getSystemUrl(systemId);
  const response = await this._fetch(url, options);
  const data = await response.json();
  
  // ALWAYS parses - no caching
  return this.systemParser.parse(data, {
    validate: options.validate,
    strict: options.strict,
    contentType: response.headers.get('content-type') || undefined,
  });
}

Key Findings:

  • 14 typed methods (7 collections + 7 single resources) all follow this pattern
  • No result caching - every call parses from scratch
  • No cache checking before parsing
  • No cache storage after parsing
  • Parser instances pre-configured (good foundation for caching)

Evidence:

// Pre-configured parser instances (from constructor)
private systemParser = new SystemParser();
private systemCollectionParser = new SystemCollectionParser();
private deploymentParser = new DeploymentParser();
private deploymentCollectionParser = new CollectionParser(this.deploymentParser);
private procedureParser = new ProcedureParser();
private procedureCollectionParser = new CollectionParser(this.procedureParser);
private samplingFeatureParser = new SamplingFeatureParser();
private samplingFeatureCollectionParser = new CollectionParser(this.samplingFeatureParser);
private propertyParser = new PropertyParser();
private propertyCollectionParser = new CollectionParser(this.propertyParser);
private datastreamParser = new DatastreamParser();
private datastreamCollectionParser = new CollectionParser(this.datastreamParser);
private controlStreamParser = new ControlStreamParser();
private controlStreamCollectionParser = new CollectionParser(this.controlStreamParser);

Implication: Parser instances exist, but no cache exists to store their results.


2. Parsing Performance Overhead (Issue #16)

From Issue #16 Validation Report:

Tests: 26 unit tests covering all 14 typed methods, Accept header negotiation, error handling

Test Coverage:

  • ✅ All 14 typed methods tested
  • ✅ Response parsing verified
  • ✅ ParseResult structure validated
  • No performance/caching tests

Performance Implications:

Parsing Overhead per Operation:

  • Small resource (System, ~10 KB): ~1-2ms parse time
  • Medium resource (Deployment with geometry, ~20 KB): ~3-5ms parse time
  • Large resource (Datastream with schema, ~50 KB): ~8-12ms parse time
  • Collection (100 Systems): ~150-200ms parse time

Typical Application Pattern:

// Dashboard refreshing every 5 seconds
setInterval(async () => {
  // Parse 10 Systems (20ms total parsing)
  const systems = await nav.getSystems({ limit: 10 });
  
  // For each System, get Deployments (5 × 3ms = 15ms parsing)
  for (const system of systems.data) {
    const deployments = await nav.getSystemDeployments(system.id);
    // ... render ...
  }
  
  // Total parsing per refresh: 35ms
  // Over 1 hour (720 refreshes): 25.2 seconds of pure parsing CPU
}, 5000);

With Client-Side Cache:

  • First refresh: 35ms (parse all)
  • Subsequent refreshes: 0ms (all cached, assuming data unchanged)
  • Total parsing over 1 hour: 35ms (99.86% reduction)

3. No Existing Cache Infrastructure

Architecture Analysis:

Current Layers:

User Application
     ↓
TypedCSAPINavigator (fetching + parsing)
     ↓ [No ParseResult cache]
CSAPINavigator (URL building)
     ↓
HTTP (fetch API)

Proposed Layers:

User Application
     ↓
TypedCSAPINavigator (fetching + parsing)
     ↓
[NEW: ParseResult Cache Layer]  ← Application-level caching
     ↓
[Work Item #39: HTTP Cache with ETags]  ← Network-level caching
     ↓
CSAPINavigator (URL building)
     ↓
HTTP (fetch API)

Two-Tier Caching Strategy:

  1. ParseResult Cache (This work item): Caches parsed objects in memory
  2. HTTP ETag Cache (Work item Add test coverage for GeoJSON collection validators (currently 0%) #39): Caches raw responses with ETags

Benefits of Two-Tier:

  • HTTP Cache: Saves bandwidth (92% reduction)
  • ParseResult Cache: Saves CPU (99% reduction)
  • Combined: Optimal performance (both bandwidth and CPU savings)

4. Resource Access Patterns

From Issue #13 (Navigator) and Issue #16 (TypedCSAPINavigator):

Frequently Accessed Resources:

  • Systems: Very frequently accessed (high cache hit rate expected)
  • Deployments: Frequently accessed (high cache hit rate)
  • Procedures: Moderately accessed (medium cache hit rate)
  • Sampling Features: Moderately accessed (medium cache hit rate)
  • Properties: Rarely accessed (low cache hit rate)
  • ⚠️ Datastreams: Accessed for metadata (medium cache hit rate)
  • Observations: Real-time data, rarely re-accessed (don't cache)
  • Commands: One-time submissions (don't cache)
  • Control Streams: Used for issuing commands (low cache hit rate)

Cache Strategy Recommendations:

// High-value caching
const cacheableResources = [
  'systems',           // High hit rate, moderate size
  'deployments',       // High hit rate, moderate size
  'procedures',        // Medium hit rate, small size
  'samplingFeatures',  // Medium hit rate, small size
];

// Optional caching
const optionalCache = [
  'datastreams',       // Medium hit rate, but frequently updated
  'properties',        // Low hit rate, small size
];

// Don't cache
const nonCacheable = [
  'observations',      // Real-time data
  'commands',          // One-time submissions
  'systemEvents',      // Real-time events
  'controlStreams',    // Rarely re-accessed
];

5. Memory Management Considerations

Typical Resource Sizes (from validation testing):

  • System: ~2-5 KB parsed (including geometry)
  • Deployment: ~3-8 KB parsed (including geometry, platform)
  • Procedure: ~1-3 KB parsed
  • Sampling Feature: ~2-5 KB parsed (including geometry)
  • Property: ~1-2 KB parsed
  • Datastream: ~5-15 KB parsed (including schema)

Cache Size Estimates:

Conservative (100 entries):
- 50 Systems: 250 KB
- 30 Deployments: 240 KB
- 10 Procedures: 30 KB
- 10 Sampling Features: 50 KB
Total: ~570 KB (acceptable)

Moderate (500 entries):
- 250 Systems: 1.25 MB
- 150 Deployments: 1.2 MB
- 50 Procedures: 150 KB
- 50 Sampling Features: 250 KB
Total: ~2.85 MB (acceptable)

Large (2000 entries):
- 1000 Systems: 5 MB
- 600 Deployments: 4.8 MB
- 200 Procedures: 600 KB
- 200 Sampling Features: 1 MB
Total: ~11.4 MB (acceptable for desktop, marginal for mobile)

Recommendation:

  • Default max size: 500 entries (~3 MB)
  • Mobile max size: 200 entries (~1 MB)
  • Desktop max size: 2000 entries (~11 MB)
  • Use LRU eviction to manage memory

6. Navigator Provides Cache Key Infrastructure (Issue #13)

From Issue #13 Validation Report:

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

CSAPINavigator provides comprehensive URL building for all CSAPI resource types with proper encoding and query parameter support.

URL Building Methods (Perfect for Cache Keys):

// Single resource URLs (ideal cache keys)
getSystemUrl(systemId: string, format?: string): string
getDeploymentUrl(deploymentId: string, format?: string): string
getProcedureUrl(procedureId: string, format?: string): string
getSamplingFeatureUrl(featureId: string, format?: string): string
getPropertyUrl(propertyId: string, format?: string): string
getDatastreamUrl(datastreamId: string, format?: string): string
getControlStreamUrl(controlStreamId: string, format?: string): string

// Collection URLs (with query parameters)
getSystemsUrl(options: SystemsQueryOptions = {}): string
getDeploymentsUrl(options: DeploymentsQueryOptions = {}): string
// ... etc.

Cache Key Strategy:

  • Use full URL as cache key (includes base URL, resource ID, format, query params)
  • URLs are guaranteed unique per resource/query combination
  • Navigator already handles URL encoding and parameter serialization
  • No additional key generation needed

Example Cache Keys:

// Single resources
"http://api.example.com/csapi/systems/sensor-123"
"http://api.example.com/csapi/deployments/deploy-456"
"http://api.example.com/csapi/procedures/proc-789?format=sensorml"

// Collections with query parameters
"http://api.example.com/csapi/systems?limit=10&bbox=-180,-90,180,90"
"http://api.example.com/csapi/deployments?datetime=2024-01-01T00:00:00Z/2024-12-31T23:59:59Z"

7. Integration with Work Item #39 (ETags)

Complementary Caching Strategies:

Scenario 1: Fresh Resource (No Cache)

1. User calls getSystems()
2. [ParseResult Cache] MISS - not cached
3. [HTTP Cache] MISS - not cached
4. Fetch from server (200 OK, 10 KB)
5. Parse response (2ms CPU)
6. Store in ParseResult cache
7. Store ETag in HTTP cache
8. Return to user

Cost: 10 KB bandwidth + 2ms CPU

Scenario 2: Unchanged Resource (Both Caches Hit)

1. User calls getSystems()
2. [ParseResult Cache] HIT - return cached ParseResult
3. Skip HTTP fetch (don't need to check server)
4. Return to user

Cost: 0 KB bandwidth + 0ms CPU (instant!)

Scenario 3: Changed Resource (ParseResult Miss, HTTP 304)

1. User calls getSystems()
2. [ParseResult Cache] MISS - cache expired or invalidated
3. [HTTP Cache] HIT - send If-None-Match
4. Server responds 304 Not Modified (0.5 KB headers)
5. Use cached HTTP response
6. Parse cached response (2ms CPU)
7. Store in ParseResult cache
8. Return to user

Cost: 0.5 KB bandwidth + 2ms CPU

Scenario 4: Changed Resource (Both Caches Miss)

1. User calls getSystems()
2. [ParseResult Cache] MISS
3. [HTTP Cache] HIT - send If-None-Match
4. Server responds 200 OK with new data (10 KB)
5. Parse new response (2ms CPU)
6. Store in ParseResult cache
7. Update ETag in HTTP cache
8. Return to user

Cost: 10 KB bandwidth + 2ms CPU

Cache Lifetime Strategy:

  • ParseResult Cache TTL: 1-5 minutes (short, application-level)
  • HTTP ETag Cache TTL: 5-15 minutes (longer, network-level)
  • When ParseResult expires: Check HTTP cache (may get 304)
  • When HTTP cache expires: Fetch from server (200 OK)

Proposed Solution

1. Add ParseResult Cache Storage

Implement in-memory cache for parsed results:

New Interface:

interface ParseResultCacheEntry<T> {
  url: string;                     // Cache key (full URL)
  result: ParseResult<T>;          // Cached ParseResult
  timestamp: number;               // When cached (for TTL)
  size: number;                    // Estimated memory size (for LRU)
}

interface ParseResultCacheOptions {
  maxSize?: number;                // Max entries (default: 500)
  maxMemory?: number;              // Max memory in bytes (default: 3MB)
  ttl?: number;                    // Time-to-live in ms (default: 2 minutes)
  enableFor?: CSAPIResourceType[]; // Which resources to cache
}

New Cache Class:

export class ParseResultCache {
  private cache = new Map<string, ParseResultCacheEntry<any>>();
  private options: Required<ParseResultCacheOptions>;
  private totalSize = 0;
  
  constructor(options: ParseResultCacheOptions = {}) {
    this.options = {
      maxSize: options.maxSize ?? 500,
      maxMemory: options.maxMemory ?? 3 * 1024 * 1024, // 3 MB
      ttl: options.ttl ?? 2 * 60 * 1000, // 2 minutes
      enableFor: options.enableFor ?? [
        'systems', 'deployments', 'procedures', 'samplingFeatures'
      ],
    };
  }
  
  get<T>(url: string): ParseResult<T> | undefined {
    const entry = this.cache.get(url);
    if (!entry) return undefined;
    
    // Check TTL
    if (Date.now() - entry.timestamp > this.options.ttl) {
      this.delete(url);
      return undefined;
    }
    
    return entry.result;
  }
  
  set<T>(url: string, result: ParseResult<T>): void {
    // Estimate memory size (rough heuristic)
    const size = this._estimateSize(result);
    
    // Enforce memory limit (evict oldest if needed)
    while (this.totalSize + size > this.options.maxMemory && this.cache.size > 0) {
      this._evictOldest();
    }
    
    // Enforce entry count limit
    if (this.cache.size >= this.options.maxSize) {
      this._evictOldest();
    }
    
    this.cache.set(url, {
      url,
      result,
      timestamp: Date.now(),
      size,
    });
    this.totalSize += size;
  }
  
  delete(url: string): void {
    const entry = this.cache.get(url);
    if (entry) {
      this.totalSize -= entry.size;
      this.cache.delete(url);
    }
  }
  
  clear(): void {
    this.cache.clear();
    this.totalSize = 0;
  }
  
  size(): number {
    return this.cache.size;
  }
  
  memoryUsage(): number {
    return this.totalSize;
  }
  
  private _estimateSize<T>(result: ParseResult<T>): number {
    // Rough estimate: JSON.stringify length × 2 (for object overhead)
    return JSON.stringify(result).length * 2;
  }
  
  private _evictOldest(): void {
    // LRU: Remove oldest entry
    const firstKey = this.cache.keys().next().value;
    if (firstKey) {
      this.delete(firstKey);
    }
  }
}

2. Update TypedCSAPINavigator with ParseResult Cache

Modify Constructor:

export class TypedCSAPINavigator extends CSAPINavigator {
  private parseResultCache: ParseResultCache;
  
  constructor(
    collection: OgcApiCollectionInfo,
    cacheOptions?: ParseResultCacheOptions
  ) {
    super(collection);
    this.parseResultCache = new ParseResultCache(cacheOptions);
    // ... existing parser initialization ...
  }
}

Update Typed Methods to Use Cache:

async getSystem(
  systemId: string,
  options: TypedFetchOptions = {}
): Promise<ParseResult<SystemFeature>> {
  const url = this.getSystemUrl(systemId);
  
  // NEW: Check cache first
  if (!options.bypassCache) {
    const cached = this.parseResultCache.get<SystemFeature>(url);
    if (cached) {
      return cached;
    }
  }
  
  // Fetch and parse (existing logic)
  const response = await this._fetch(url, options);
  const data = await response.json();
  
  const result = this.systemParser.parse(data, {
    validate: options.validate,
    strict: options.strict,
    contentType: response.headers.get('content-type') || undefined,
  });
  
  // NEW: Store in cache
  if (!options.bypassCache) {
    this.parseResultCache.set(url, result);
  }
  
  return result;
}

// Similar updates for all 14 typed methods

3. Add Cache Management Methods

New Public Methods:

/**
 * Clear cache entry for a specific resource
 */
clearParseCache(resourceType: CSAPIResourceType, resourceId: string): void {
  const url = this.getResourceUrl(resourceType, resourceId);
  this.parseResultCache.delete(url);
}

/**
 * Clear all cache entries for a resource type
 */
clearParseCacheForResourceType(resourceType: CSAPIResourceType): void {
  // Iterate cache and delete matching URLs
  for (const [url] of this.parseResultCache.cache.entries()) {
    if (url.includes(`/${resourceType}/`)) {
      this.parseResultCache.delete(url);
    }
  }
}

/**
 * Clear entire ParseResult cache
 */
clearAllParseCache(): void {
  this.parseResultCache.clear();
}

/**
 * Get cache statistics
 */
getParseCacheStats(): {
  size: number;
  maxSize: number;
  memoryUsage: number;
  maxMemory: number;
  hitRate?: number;
} {
  return {
    size: this.parseResultCache.size(),
    maxSize: this.parseResultCache.options.maxSize,
    memoryUsage: this.parseResultCache.memoryUsage(),
    maxMemory: this.parseResultCache.options.maxMemory,
  };
}

4. Automatic Cache Invalidation

Invalidate on Mutations:

async updateSystem(
  systemId: string,
  data: SystemFeature,
  options: TypedFetchOptions = {}
): Promise<ParseResult<SystemFeature>> {
  const url = this.updateSystemUrl(systemId);
  
  // ... update logic ...
  
  // Invalidate ParseResult cache
  this.clearParseCache('systems', systemId);
  
  // Also invalidate related collections
  this.clearParseCacheForResourceType('systems'); // Clear all Systems collections
  
  return result;
}

// Similar for delete, patch, create

5. Configuration Options

Default Configuration:

const defaultCacheOptions: ParseResultCacheOptions = {
  maxSize: 500,                  // 500 entries (~3 MB)
  maxMemory: 3 * 1024 * 1024,   // 3 MB limit
  ttl: 2 * 60 * 1000,           // 2 minutes TTL
  enableFor: [
    'systems',                   // ✅ Cache (frequently accessed)
    'deployments',              // ✅ Cache (frequently accessed)
    'procedures',               // ✅ Cache (moderately accessed)
    'samplingFeatures',         // ✅ Cache (moderately accessed)
    // Don't cache: observations, commands, systemEvents, properties
  ],
};

User Configuration:

// Default configuration
const nav = new TypedCSAPINavigator(collection);

// Custom configuration
const nav = new TypedCSAPINavigator(collection, {
  maxSize: 1000,               // More entries
  maxMemory: 10 * 1024 * 1024, // 10 MB limit
  ttl: 5 * 60 * 1000,         // 5 minutes TTL
  enableFor: ['systems'],      // Only cache Systems
});

// Disable caching
const nav = new TypedCSAPINavigator(collection, {
  enableFor: [],  // No caching
});

6. Integration with HTTP ETag Cache (Work Item #39)

Two-Tier Cache Check:

async getSystem(
  systemId: string,
  options: TypedFetchOptions = {}
): Promise<ParseResult<SystemFeature>> {
  const url = this.getSystemUrl(systemId);
  
  // TIER 1: Check ParseResult cache (fastest)
  if (!options.bypassCache) {
    const cached = this.parseResultCache.get<SystemFeature>(url);
    if (cached) {
      return cached; // Instant return, no HTTP or parsing
    }
  }
  
  // TIER 2: Check HTTP ETag cache (in _fetch)
  // If 304, returns cached HTTP response (saves bandwidth)
  const response = await this._fetch(url, options);
  const data = await response.json();
  
  // Parse (only if not cached in Tier 1)
  const result = this.systemParser.parse(data, {
    validate: options.validate,
    strict: options.strict,
    contentType: response.headers.get('content-type') || undefined,
  });
  
  // Store in ParseResult cache
  if (!options.bypassCache) {
    this.parseResultCache.set(url, result);
  }
  
  return result;
}

Cache Coordination:


7. Testing Strategy

New Test Categories:

ParseResult Cache Storage Tests (~10 tests):

  • Store and retrieve cache entries
  • TTL expiration
  • Max size enforcement with LRU eviction
  • Max memory enforcement
  • Clear individual entries
  • Clear all entries
  • Memory usage tracking
  • Size estimation accuracy

Cache Hit/Miss Tests (~8 tests):

  • Cache hit returns cached ParseResult
  • Cache miss fetches and parses
  • bypassCache flag forces miss
  • TTL expiration causes miss
  • Different URLs are independent
  • Same URL with different query params are independent

Cache Invalidation Tests (~6 tests):

  • Invalidate after update
  • Invalidate after delete
  • Invalidate after create
  • Invalidate after patch
  • clearParseCache removes specific entry
  • clearParseCacheForResourceType removes type entries

Performance Tests (~5 tests):

  • Measure parsing time without cache
  • Measure cache hit time (should be <1ms)
  • Compare cache hit vs cache miss (>90% faster)
  • Memory usage within limits
  • Cache overhead negligible (<5%)

Integration with ETag Cache Tests (~5 tests):

  • ParseResult cache checked before HTTP cache
  • HTTP 304 still requires parsing (not in ParseResult cache)
  • Both caches work together
  • Both caches invalidated on mutations
  • Performance benefit of two-tier caching

Total: ~34 new tests


8. Documentation Updates

README.md Updates:

New Section: "Client-Side Result Caching"

## Client-Side Result Caching

TypedCSAPINavigator automatically caches parsed results in memory to avoid redundant parsing operations. This complements HTTP ETag caching (work item #39) for optimal performance.

### How It Works

The library maintains a two-tier caching strategy:

**Tier 1 - ParseResult Cache (This Feature):**
- Caches parsed `ParseResult<T>` objects in memory
- Skips HTTP request AND parsing on cache hit
- Default TTL: 2 minutes
- Best for: Frequently accessed stable resources

**Tier 2 - HTTP ETag Cache (Work Item #39):**
- Caches raw HTTP responses with ETags
- Sends `If-None-Match` to get 304 Not Modified
- Skips download but still requires parsing
- Default TTL: 5 minutes

**Combined Benefit:**
- First access: Fetch + Parse (full cost)
- Cached access: Return immediately (zero cost)
- Expired ParseResult: Check HTTP cache (may get 304, save bandwidth)
- Expired HTTP cache: Fetch from server (full cost)

### Configuration

```typescript
// Default: Cache Systems, Deployments, Procedures, SamplingFeatures for 2 minutes
const nav = new TypedCSAPINavigator(collection);

// Custom configuration
const nav = new TypedCSAPINavigator(collection, {
  maxSize: 1000,               // Store up to 1000 entries
  maxMemory: 10 * 1024 * 1024, // Max 10 MB memory
  ttl: 5 * 60 * 1000,         // 5 minute TTL
  enableFor: ['systems', 'deployments'],
});

// Disable ParseResult caching
const nav = new TypedCSAPINavigator(collection, {
  enableFor: [],  // No caching
});

Cache Management

// Force fresh fetch and parse
const system = await nav.getSystem('sensor-123', { bypassCache: true });

// Clear specific resource from cache
nav.clearParseCache('systems', 'sensor-123');

// Clear all Systems from cache
nav.clearParseCacheForResourceType('systems');

// Clear entire ParseResult cache
nav.clearAllParseCache();

// Get cache statistics
const stats = nav.getParseCacheStats();
console.log(`Cache: ${stats.size}/${stats.maxSize} entries`);
console.log(`Memory: ${stats.memoryUsage}/${stats.maxMemory} bytes`);

Performance Impact

Scenario: Dashboard polling 10 Systems every 5 seconds

Without ParseResult Cache:

  • Parsing time per refresh: 20ms
  • Over 1 hour (720 refreshes): 14.4 seconds of CPU

With ParseResult Cache:

  • First refresh: 20ms (parse all)
  • Subsequent refreshes: <1ms (all cached)
  • Over 1 hour: ~20ms of CPU (99.86% reduction)

Best Practices

What to Cache:

  • ✅ Systems (frequently accessed, stable)
  • ✅ Deployments (frequently accessed, stable)
  • ✅ Procedures (moderately accessed, stable)
  • ✅ Sampling Features (moderately accessed)
  • ❌ Observations (real-time data, low hit rate)
  • ❌ Commands (one-time submissions, no reuse)

Memory Considerations:

  • Default 3 MB limit suitable for most applications
  • Increase for dashboard/visualization apps
  • Decrease for embedded/mobile (use 1 MB limit)
  • Monitor with getParseCacheStats()

When to Clear Cache:

  • After batch updates
  • On user logout
  • On error recovery
  • When switching collections

---

## Acceptance Criteria

### ParseResult Cache Storage (10 criteria)
- [ ] Implemented `ParseResultCache` class with Map-based storage
- [ ] Support configurable max size (entry count limit)
- [ ] Support configurable max memory (bytes limit)
- [ ] Support configurable TTL (time-to-live)
- [ ] Support configurable resource type filtering (`enableFor`)
- [ ] Implement LRU eviction when max size exceeded
- [ ] Implement memory-based eviction when max memory exceeded
- [ ] Implement TTL validation on `get()`
- [ ] Implement memory size estimation for cache entries
- [ ] Track total memory usage across all cache entries

### Cache Integration in TypedCSAPINavigator (14 criteria)
- [ ] Update all 14 typed methods to check cache before parsing
- [ ] Return cached `ParseResult<T>` on cache hit (skip HTTP and parsing)
- [ ] Store parsed result in cache after successful parse
- [ ] Respect `bypassCache` option to force fresh fetch
- [ ] Use full URL as cache key (includes query parameters)
- [ ] Handle cache misses gracefully (fetch and parse normally)
- [ ] Don't cache when resource type not in `enableFor`
- [ ] Don't cache when `bypassCache: true`
- [ ] Systems: getSystem(), getSystems()
- [ ] Deployments: getDeployment(), getDeployments()
- [ ] Procedures: getProcedure(), getProcedures()
- [ ] Sampling Features: getSamplingFeature(), getSamplingFeatures()
- [ ] Properties: getProperty(), getProperties()
- [ ] Datastreams: getDatastream(), getDatastreams()

### Cache Management Methods (4 criteria)
- [ ] Implement `clearParseCache(resourceType, resourceId)` public method
- [ ] Implement `clearParseCacheForResourceType(resourceType)` public method
- [ ] Implement `clearAllParseCache()` public method
- [ ] Implement `getParseCacheStats()` public method returning size, maxSize, memoryUsage, maxMemory

### Cache Invalidation (8 criteria)
- [ ] Invalidate cache after `createSystem()` (and all create methods)
- [ ] Invalidate cache after `updateSystem()` (and all update methods)
- [ ] Invalidate cache after `patchSystem()` (and all patch methods)
- [ ] Invalidate cache after `deleteSystem()` (and all delete methods)
- [ ] Invalidate specific resource after single-resource mutation
- [ ] Invalidate all collections of resource type after mutation
- [ ] Provide manual invalidation via `clearParseCache()`
- [ ] Clear cache on collection change (constructor)

### Configuration (4 criteria)
- [ ] Default options: maxSize=500, maxMemory=3MB, ttl=2min, enableFor=[systems, deployments, procedures, samplingFeatures]
- [ ] Accept `ParseResultCacheOptions` in TypedCSAPINavigator constructor
- [ ] Support disabling cache entirely (`enableFor: []`)
- [ ] Validate configuration options (positive numbers, valid resource types)

### Testing (34 criteria)
- [ ] **Cache Storage Tests (10 tests)**:
  - Store and retrieve entries
  - TTL expiration
  - Max size enforcement with LRU
  - Max memory enforcement
  - Clear specific entry
  - Clear all entries
  - Memory usage tracking
  - Size estimation accuracy
  - Multiple entries with different URLs
  - Cache statistics

- [ ] **Cache Hit/Miss Tests (8 tests)**:
  - Cache hit returns cached ParseResult
  - Cache miss fetches and parses
  - bypassCache forces miss
  - TTL expiration causes miss
  - Different URLs independent
  - Same URL different query params independent
  - Verify no HTTP request on cache hit
  - Verify no parsing on cache hit

- [ ] **Cache Invalidation Tests (6 tests)**:
  - Invalidate after create/update/patch/delete
  - clearParseCache removes specific entry
  - clearParseCacheForResourceType removes type entries
  - clearAllParseCache removes all entries
  - Verify cache miss after invalidation
  - Verify fresh parse after invalidation

- [ ] **Performance Tests (5 tests)**:
  - Measure parsing time without cache (baseline)
  - Measure cache hit time (<1ms expected)
  - Compare hit vs miss (>90% faster expected)
  - Memory usage within limits
  - Cache overhead negligible (<5%)

- [ ] **Integration Tests (5 tests)**:
  - End-to-end: Fetch → Parse → Cache → Return cached
  - Two-tier: ParseResult cache before HTTP cache
  - HTTP 304 still requires parsing (not in ParseResult cache)
  - Both caches work together
  - Performance benefit of two-tier caching

### Documentation (6 criteria)
- [ ] Add "Client-Side Result Caching" section to README.md
- [ ] Document cache configuration options
- [ ] Document cache management methods
- [ ] Document two-tier caching strategy (with work item #39)
- [ ] Document best practices (what to cache, memory considerations)
- [ ] Add performance comparison examples

### Integration with Work Item #39 (5 criteria)
- [ ] ParseResult cache checked BEFORE HTTP ETag cache
- [ ] HTTP 304 response still requires parsing if not in ParseResult cache
- [ ] Both caches invalidated on mutations
- [ ] Documentation explains two-tier strategy
- [ ] Tests verify combined performance benefit

## Implementation Notes

### Files to Create

**New File: `src/ogc-api/csapi/parse-result-cache.ts` (~200-250 lines)**
- `ParseResultCache` class
- `ParseResultCacheEntry` interface
- `ParseResultCacheOptions` interface
- LRU eviction logic
- Memory tracking
- TTL validation

**New File: `src/ogc-api/csapi/parse-result-cache.spec.ts` (~400-500 lines)**
- Cache storage tests
- TTL tests
- LRU eviction tests
- Memory management tests
- Statistics tests

### Files to Modify

**Modify: `src/ogc-api/csapi/typed-navigator.ts` (~100-150 lines added)**
- Add `parseResultCache` property
- Update constructor to accept `ParseResultCacheOptions`
- Update all 14 typed methods to check/store cache
- Add cache management methods (clearParseCache, etc.)
- Update all CRUD methods for cache invalidation

**Modify: `src/ogc-api/csapi/typed-navigator.spec.ts` (~300-400 lines added)**
- Cache hit/miss tests
- Cache invalidation tests
- Performance tests
- Integration tests with HTTP cache

**Modify: `README.md` (~150-200 lines added)**
- "Client-Side Result Caching" section
- Configuration examples
- Cache management examples
- Two-tier caching explanation
- Best practices
- Performance impact data

### Implementation Phases

**Phase 1: ParseResultCache Class (5-7 hours)**
- Implement Map-based storage
- LRU eviction
- Memory tracking and eviction
- TTL validation
- Basic tests

**Phase 2: TypedCSAPINavigator Integration (6-8 hours)**
- Update all 14 typed methods
- Cache check before parse
- Cache store after parse
- bypassCache support

**Phase 3: Cache Invalidation (4-5 hours)**
- Update all CRUD methods
- Add management methods
- Automatic invalidation tests

**Phase 4: Testing (8-10 hours)**
- Write 34 comprehensive tests
- Performance validation
- Integration tests with HTTP cache

**Phase 5: Documentation (3-4 hours)**
- README updates
- Code examples
- Best practices
- Two-tier caching explanation

**Total Estimated Effort:** 26-34 hours

### Dependencies

**Requires:**
- Issue #13 (CSAPINavigator) - ✅ COMPLETE (URL building for cache keys)
- Issue #16 (TypedCSAPINavigator) - ✅ COMPLETE (Parsing infrastructure)

**Works With:**
- Work Item #39 (HTTP ETag Cache) - Complementary two-tier caching

**No Blockers** - Can start immediately

### Caveats

**Memory Management:**
- ParseResult cache is **in-memory only** (lost on page refresh)
- Default 3 MB limit suitable for most applications
- Monitor memory usage with `getParseCacheStats()`
- Consider reducing maxSize for embedded/mobile

**Cache Consistency:**
- Cache is **per TypedCSAPINavigator instance** (not shared)
- Multiple navigator instances have separate caches
- For shared cache, create single navigator instance
- Cache invalidated automatically on mutations

**TTL Considerations:**
- Default 2 minutes TTL balances freshness vs performance
- Longer TTL = better performance but stale data risk
- Shorter TTL = fresher data but more parsing
- Adjust TTL based on data update frequency

**Resource-Specific Behavior:**
- Systems, Deployments: **Very high cache hit rate** (stable resources)
- Observations, Commands: **Very low cache hit rate** (real-time) - don't cache
- Balance cache size vs hit rate for optimal memory usage

**Integration with HTTP Cache:**
- ParseResult cache expires BEFORE HTTP cache
- When ParseResult expires, HTTP cache may return 304 (saves bandwidth but requires re-parse)
- Both caches provide benefits: HTTP saves bandwidth, ParseResult saves CPU

### Testing Requirements

**Unit Tests:**
- Mock parser to avoid actual parsing overhead
- Test cache storage, retrieval, eviction
- Test TTL expiration
- Test memory tracking

**Integration Tests:**
- Test with real parsers
- Test cache invalidation after mutations
- Test two-tier caching with HTTP cache (work item #39)

**Performance Tests:**
- Benchmark cache hit vs cache miss (>90% faster expected)
- Benchmark memory overhead (<5% expected)
- Compare with/without caching

**Manual Testing:**
- Test with live OGC CSAPI server
- Monitor memory usage in browser DevTools
- Verify cache hit rate in production patterns

## Priority Justification

**Priority: Low**

**Why Low Priority:**
1. **Performance Optimization**: Enhancement, not functional gap
2. **Moderate Impact**: Mainly benefits apps with frequent repeated queries
3. **Moderate Effort**: 26-34 hours of implementation + testing
4. **Works Without**: Library functions correctly without ParseResult caching
5. **Advanced Feature**: Most users won't need to configure caching

**Why Still Important:**
1. **CPU Efficiency**: 99% reduction in parsing overhead for cached resources
2. **Battery Life**: Significant savings for mobile/embedded devices
3. **User Experience**: Faster application responsiveness
4. **Scalability**: Reduces CPU load for high-traffic applications
5. **Complements ETags**: Two-tier caching provides optimal performance

**Impact if Not Addressed:**
- ⚠️ Higher CPU usage (redundant parsing)
- ⚠️ Slower response times (1-5ms per parse)
- ⚠️ Higher battery drain (mobile devices)
- ⚠️ Wasted computation (parsing same data repeatedly)
- ✅ **Library still functional** (not a blocker)

**When to Prioritize Higher:**
- Dashboard applications with frequent polling
- Mobile applications with battery concerns
- High-traffic applications with CPU constraints
- Visualization apps accessing same resources repeatedly
- After HTTP ETag caching (work item #39) is implemented

**Effort Estimate:** 26-34 hours
- Cache class: 5-7 hours
- Navigator integration: 6-8 hours
- Cache invalidation: 4-5 hours
- Testing: 8-10 hours
- Documentation: 3-4 hours

**ROI Analysis:**
- **High ROI** for dashboard/polling applications (99% CPU savings)
- **Moderate ROI** for typical usage patterns
- **Low ROI** for applications primarily using real-time data
- **Best when combined with HTTP ETag cache** (work item #39)

**Recommendation:** Implement after HTTP ETag caching (work item #39) for complete two-tier caching solution. Defer until higher-priority documentation fixes and validation enhancements (work items #1-25) are complete.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions