Skip to content

Implement ETags and conditional requests for HTTP caching #62

Description

@Sam-Bolling

Problem

The ogc-client-CSAPI implementation has no HTTP caching mechanism using ETags and conditional requests. This results in unnecessary network traffic and bandwidth waste when fetching unchanged CSAPI resources.

Current State:

  • TypedCSAPINavigator makes fresh HTTP requests every time
  • No caching of ETag values from previous responses
  • No If-None-Match headers sent on subsequent requests
  • No handling of 304 Not Modified responses
  • Every request fetches complete resource data even if unchanged

Real-World Impact:

  • Network Inefficiency: Fetching unchanged 10KB Systems multiple times wastes bandwidth
  • Server Load: Server must serialize and send full responses even when data unchanged
  • Latency: Unnecessary parsing and data transfer delays response time
  • Mobile/Slow Connections: Particularly problematic for bandwidth-constrained environments
  • Cost: Metered connections pay for duplicate data transfer

Example Scenario:

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

// First fetch: 10KB System feature
const system1 = await nav.getSystem('sensor-123');

// Second fetch: Another 10KB download (even if unchanged)
const system2 = await nav.getSystem('sensor-123');

// Third fetch: Yet another 10KB download
const system3 = await nav.getSystem('sensor-123');

// Total: 30KB downloaded when 10KB would suffice

With ETags (Desired Behavior):

// First fetch: 10KB download + ETag stored
const system1 = await nav.getSystem('sensor-123');

// Second fetch: 304 Not Modified (0 bytes downloaded)
const system2 = await nav.getSystem('sensor-123');

// Third fetch: 304 Not Modified (0 bytes downloaded)
const system3 = await nav.getSystem('sensor-123');

// Total: 10KB downloaded (67% bandwidth savings)

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

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

Validated Commit: a71706b9592cad7a5ad06e6cf8ddc41fa5387732

Detailed Findings

1. Navigator Provides URL Building Only (Issue #13)

From Issue #13 Validation Report:

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

CSAPINavigator is a Resource-Oriented URL Builder Pattern with 10 resource types (Systems, Procedures, Deployments, Sampling Features, Properties, Datastreams, Observations, Commands, Control Streams, System Events) and full CRUD operations.

Architecture:

  1. Initialization Layer: Analyzes OGC API collection metadata
  2. Resource Layer: CRUD methods for 10 resource types
  3. Query Parameter Layer: Spatial, temporal, semantic, structural, pagination, projection
  4. Helper Method Layer: Resource availability checking, URL construction, parameter serialization
  5. Relationship Layer: System sub-resources, deployment sub-resources, datastream/control stream sub-resources

Key Finding: CSAPINavigator does NOT handle HTTP requests - it only builds URLs. No caching infrastructure exists at this level.

Evidence:

// CSAPINavigator methods return strings, not responses
getSystemsUrl(options: SystemsQueryOptions = {}): string
getSystemUrl(systemId: string, format?: string): string
createSystemUrl(): string
updateSystemUrl(systemId: string): string
// ... etc.

Implication: Caching must be implemented at the HTTP client layer, not the Navigator layer.


2. TypedCSAPINavigator Has Simple Fetch (Issue #16)

From Issue #16 Validation Report:

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

TypedCSAPINavigator extends CSAPINavigator and adds:

  1. Automatic HTTP fetching
  2. Response parsing with format detection
  3. Type-safe return values
  4. Built-in validation options
  5. Error handling

Current _fetch() Implementation:

private async _fetch(
  url: string,
  options: TypedFetchOptions = {}
): Promise<Response> {
  const fetchFn = options.fetch || fetch;
  const headers: Record<string, string> = {
    ...options.headers,
  };
  
  // Set Accept header based on supported formats
  if (options.accept) {
    headers['Accept'] = options.accept;
  } else if (this.supportedFormats.size > 0) {
    // Prefer GeoJSON, then SensorML, then SWE
    if (this.supportedFormats.has('application/geo+json')) {
      headers['Accept'] = 'application/geo+json';
    } else if (this.supportedFormats.has('application/sml+json')) {
      headers['Accept'] = 'application/sml+json';
    } else if (this.supportedFormats.has('application/swe+json')) {
      headers['Accept'] = 'application/swe+json';
    } else {
      headers['Accept'] = 'application/json';
    }
  } else {
    headers['Accept'] = 'application/json';
  }
  
  const response = await fetchFn(url, { headers });
  
  if (!response.ok) {
    throw new Error(
      `HTTP ${response.status}: ${response.statusText} (${url})`
    );
  }

  return response;
}

Key Findings:

  • ✅ Accept header negotiation implemented
  • ✅ Custom fetch function support (useful for testing)
  • ✅ Custom headers support
  • ✅ HTTP error handling
  • No ETag caching
  • No If-None-Match headers
  • No 304 Not Modified handling
  • No cache storage

Evidence - Typical Method:

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

Every call to getSystem() makes a fresh HTTP request, even if the resource hasn't changed.


3. No Existing Cache Infrastructure

Architecture Analysis:

Current Layers:

User Application
     ↓
TypedCSAPINavigator (fetching + parsing)
     ↓
CSAPINavigator (URL building)
     ↓
HTTP (fetch API)

Missing Layer:

User Application
     ↓
TypedCSAPINavigator (fetching + parsing)
     ↓
[MISSING: HTTP Cache Layer with ETag support]  ← NEW LAYER NEEDED
     ↓
CSAPINavigator (URL building)
     ↓
HTTP (fetch API)

What's Missing:

  • No cache storage (in-memory Map or localStorage)
  • No ETag extraction from response headers
  • No If-None-Match header insertion
  • No 304 Not Modified response handling
  • No cache invalidation logic

4. HTTP Caching Standards (RFC 7232)

How ETags Work:

First Request (Cache Miss):

GET /csapi/systems/sensor-123 HTTP/1.1
Accept: application/geo+json

HTTP/1.1 200 OK
Content-Type: application/geo+json
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Length: 10240

{
  "type": "Feature",
  "id": "sensor-123",
  ...
}

Subsequent Request (Cache Hit - Unchanged):

GET /csapi/systems/sensor-123 HTTP/1.1
Accept: application/geo+json
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Length: 0

Subsequent Request (Cache Miss - Changed):

GET /csapi/systems/sensor-123 HTTP/1.1
Accept: application/geo+json
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

HTTP/1.1 200 OK
Content-Type: application/geo+json
ETag: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
Content-Length: 10500

{
  "type": "Feature",
  "id": "sensor-123",
  ...
}

Benefits:

  • 304 Not Modified: Tiny response (headers only, no body)
  • Bandwidth Savings: 67-95% reduction for unchanged resources
  • Server Load: Server can skip serialization for unchanged data
  • Client Speed: Faster response time (less data transfer, less parsing)

5. OGC API Caching Best Practices

From OGC API - Features Best Practices:

Servers SHOULD include ETag headers in responses to enable efficient caching via conditional requests (HTTP 304).

Clients SHOULD cache resources and use If-None-Match headers to minimize bandwidth usage.

OGC API - Connected Systems Implications:

  • ✅ Systems, Deployments, Procedures: Relatively stable (hours/days unchanged) → High cache hit rate
  • ✅ Sampling Features, Properties: Moderately stable (minutes/hours unchanged) → Good cache hit rate
  • ⚠️ Datastreams: Frequently updated (schema changes rare) → Moderate cache hit rate
  • ❌ Observations, Commands: Frequently changing (real-time data) → Low cache hit rate (not worth caching)

Recommendation:

  • Cache Systems, Deployments, Procedures, Sampling Features, Properties
  • ⚠️ Optionally cache Datastreams/Control Streams (if server provides ETags)
  • Don't cache Observations, Commands, System Events (real-time data)

6. Performance Analysis (No ETags vs. With ETags)

Scenario: Polling a System every 10 seconds for 1 hour

Without ETags (Current):

Requests: 360 (1 per 10 seconds × 60 minutes)
Data per request: 10 KB (typical System feature)
Total data transferred: 3,600 KB (3.5 MB)
Server serializations: 360
Parse operations: 360
Latency per request: ~200ms (fetch + parse)
Total time: 72 seconds (200ms × 360)

With ETags (Proposed):

Requests: 360
Cache hits (304): 350 (97% hit rate - typical for stable Systems)
Cache misses (200): 10 (3% - System updated)
Data per cache hit: 0.5 KB (headers only)
Data per cache miss: 10 KB
Total data transferred: 275 KB (175 KB headers + 100 KB data)
Server serializations: 10 (only for cache misses)
Parse operations: 10 (only for cache misses)
Latency per cache hit: ~50ms (headers only)
Latency per cache miss: ~200ms
Total time: 19.5 seconds (50ms × 350 + 200ms × 10)

Savings:

  • Bandwidth: 3,600 KB → 275 KB (92% reduction)
  • Server Load: 360 serializations → 10 (97% reduction)
  • Client CPU: 360 parses → 10 (97% reduction)
  • Latency: 72 seconds → 19.5 seconds (73% faster)

Real-World Impact:

  • Mobile user on 4G: Saves 3.3 MB per hour (~80 MB per day)
  • Server handling 100 clients: 36,000 serializations/hour → 1,000 serializations/hour (35x less load)
  • Application responsiveness: 4x faster average response time

7. Existing Test Coverage (No Cache Tests)

From Issue #13 (Navigator):

Tests: 274 unit tests, 92.7% coverage

What's Tested:

  • URL construction for all 10 resource types
  • Query parameter combinations
  • CRUD operations
  • Hierarchical resources
  • Resource relationships

What's NOT Tested:

  • HTTP caching (ETags, conditional requests)
  • Cache invalidation
  • 304 Not Modified handling

From Issue #16 (TypedCSAPINavigator):

Tests: 26 unit tests, 96.66% coverage

What's Tested:

  • All 14 typed methods (7 collections + 7 single resources)
  • Accept header negotiation (8 tests)
  • Error handling (parse errors, fetch errors)
  • Custom fetch and headers

What's NOT Tested:

  • ETag caching
  • If-None-Match headers
  • 304 Not Modified responses
  • Cache storage and retrieval
  • Cache invalidation strategies

Implication: New cache layer requires comprehensive test coverage (~20-30 new tests).


Proposed Solution

1. Add ETag Cache Storage

Implement in-memory cache for ETag storage:

New Interface:

interface CacheEntry {
  etag: string;                    // ETag from server
  url: string;                     // Full URL (cache key)
  timestamp: number;               // When cached (for TTL)
  data?: unknown;                  // Cached response data (optional)
  contentType?: string;            // Content-Type header
}

interface CacheOptions {
  maxSize?: number;                // Max cache entries (default: 1000)
  ttl?: number;                    // Time-to-live in ms (default: 5 minutes)
  enableFor?: CSAPIResourceType[]; // Which resources to cache (default: all except Observations/Commands)
}

New Cache Class:

export class HTTPCache {
  private cache = new Map<string, CacheEntry>();
  private options: Required<CacheOptions>;
  
  constructor(options: CacheOptions = {}) {
    this.options = {
      maxSize: options.maxSize ?? 1000,
      ttl: options.ttl ?? 5 * 60 * 1000, // 5 minutes default
      enableFor: options.enableFor ?? [
        'systems', 'deployments', 'procedures',
        'samplingFeatures', 'properties', 'datastreams', 'controlStreams'
      ],
    };
  }
  
  get(url: string): CacheEntry | undefined {
    const entry = this.cache.get(url);
    if (!entry) return undefined;
    
    // Check TTL
    if (Date.now() - entry.timestamp > this.options.ttl) {
      this.cache.delete(url);
      return undefined;
    }
    
    return entry;
  }
  
  set(url: string, etag: string, data?: unknown, contentType?: string): void {
    // Enforce max size (LRU eviction)
    if (this.cache.size >= this.options.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(url, {
      etag,
      url,
      timestamp: Date.now(),
      data,
      contentType,
    });
  }
  
  delete(url: string): void {
    this.cache.delete(url);
  }
  
  clear(): void {
    this.cache.clear();
  }
  
  size(): number {
    return this.cache.size;
  }
}

2. Update TypedCSAPINavigator with Cache Support

Modify TypedCSAPINavigator Constructor:

export class TypedCSAPINavigator extends CSAPINavigator {
  private httpCache: HTTPCache;
  
  constructor(
    collection: OgcApiCollectionInfo,
    cacheOptions?: CacheOptions
  ) {
    super(collection);
    this.httpCache = new HTTPCache(cacheOptions);
    // ... existing parser initialization ...
  }
  
  // ... existing methods ...
}

Update _fetch() Method:

private async _fetch(
  url: string,
  options: TypedFetchOptions = {}
): Promise<Response> {
  const fetchFn = options.fetch || fetch;
  const headers: Record<string, string> = {
    ...options.headers,
  };
  
  // Existing Accept header logic
  // ...
  
  // NEW: Add If-None-Match header if cached
  const cached = this.httpCache.get(url);
  if (cached && !options.bypassCache) {
    headers['If-None-Match'] = cached.etag;
  }
  
  const response = await fetchFn(url, { headers });
  
  // NEW: Handle 304 Not Modified
  if (response.status === 304 && cached) {
    // Return cached response
    return new Response(JSON.stringify(cached.data), {
      status: 200,
      statusText: 'OK',
      headers: {
        'Content-Type': cached.contentType || 'application/json',
        'ETag': cached.etag,
        'X-Cache': 'HIT', // Debug header
      },
    });
  }
  
  if (!response.ok) {
    throw new Error(
      `HTTP ${response.status}: ${response.statusText} (${url})`
    );
  }
  
  // NEW: Store ETag from response
  const etag = response.headers.get('etag');
  if (etag && !options.bypassCache) {
    // Clone response to read data for caching
    const clonedResponse = response.clone();
    const data = await clonedResponse.json();
    const contentType = response.headers.get('content-type') || undefined;
    
    this.httpCache.set(url, etag, data, contentType);
  }

  return response;
}

Update TypedFetchOptions:

export interface TypedFetchOptions extends ParserOptions {
  fetch?: typeof fetch;
  headers?: Record<string, string>;
  accept?: string;
  bypassCache?: boolean;  // NEW: Force fresh fetch
}

3. Add Cache Invalidation Methods

New Public Methods:

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

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

/**
 * Clear entire cache
 */
clearAllCache(): void {
  this.httpCache.clear();
}

/**
 * Get cache statistics
 */
getCacheStats(): { size: number; maxSize: number; hitRate?: number } {
  return {
    size: this.httpCache.size(),
    maxSize: this.httpCache.options.maxSize,
    // hitRate calculation requires tracking hits/misses
  };
}

4. Add Automatic Cache Invalidation on Mutations

Update CRUD Methods to Invalidate Cache:

async updateSystem(
  systemId: string,
  data: SystemFeature,
  options: TypedFetchOptions = {}
): Promise<ParseResult<SystemFeature>> {
  const url = this.updateSystemUrl(systemId);
  const response = await this._fetch(url, {
    ...options,
    method: 'PUT',
    body: JSON.stringify(data),
  });
  
  // Invalidate cache after successful update
  this.clearCache('systems', systemId);
  
  const responseData = await response.json();
  return this.systemParser.parse(responseData, {
    validate: options.validate,
    strict: options.strict,
    contentType: response.headers.get('content-type') || undefined,
  });
}

async deleteSystem(
  systemId: string,
  options: TypedFetchOptions = {}
): Promise<void> {
  const url = this.deleteSystemUrl(systemId);
  await this._fetch(url, {
    ...options,
    method: 'DELETE',
  });
  
  // Invalidate cache after successful delete
  this.clearCache('systems', systemId);
}

// Similar for patch, create, etc.

5. Configuration Options

Default Cache Behavior:

// Enable caching by default for stable resources
const defaultCacheOptions: CacheOptions = {
  maxSize: 1000,                 // Store up to 1000 resources
  ttl: 5 * 60 * 1000,           // 5 minutes TTL
  enableFor: [
    'systems',                   // ✅ Cache (stable)
    'deployments',              // ✅ Cache (stable)
    'procedures',               // ✅ Cache (stable)
    'samplingFeatures',         // ✅ Cache (stable)
    'properties',               // ✅ Cache (stable)
    'datastreams',              // ✅ Cache (moderately stable)
    'controlStreams',           // ✅ Cache (moderately stable)
    // Exclude: observations, commands, systemEvents (real-time data)
  ],
};

User Configuration:

// User can customize cache behavior
const nav = new TypedCSAPINavigator(collection, {
  maxSize: 5000,                // Larger cache
  ttl: 15 * 60 * 1000,         // 15 minutes TTL
  enableFor: ['systems', 'deployments'], // Only cache Systems and Deployments
});

// Or disable caching entirely
const nav = new TypedCSAPINavigator(collection, {
  enableFor: [],  // Empty array = no caching
});

6. Testing Strategy

New Test Categories:

Cache Storage Tests (~8 tests):

  • Store and retrieve cache entries
  • Enforce max size with LRU eviction
  • TTL expiration
  • Clear individual entries
  • Clear all entries
  • Get cache statistics

ETag Request Tests (~6 tests):

  • Send If-None-Match header on cache hit
  • Don't send If-None-Match on cache miss
  • Don't send If-None-Match when bypassCache=true
  • Extract and store ETag from 200 responses
  • Update stored ETag on 200 responses

304 Not Modified Tests (~5 tests):

  • Return cached data on 304 response
  • Don't call parser on 304 (use cached data)
  • Set X-Cache: HIT header on 304
  • Handle 304 without cached data (edge case)
  • Verify bandwidth savings on 304

Cache Invalidation Tests (~6 tests):

  • Invalidate after PUT (update)
  • Invalidate after DELETE
  • Invalidate after POST (create)
  • Invalidate after PATCH
  • Invalidate entire resource type
  • Clear all cache

Integration Tests (~5 tests):

  • End-to-end: Fetch → Cache → 304 → Return cached
  • End-to-end: Fetch → Cache → Update → Invalidate → Fresh fetch
  • Multiple resources with different ETags
  • Cache hit rate tracking
  • Performance comparison (with vs without cache)

Total: ~30 new tests


7. Documentation Updates

README.md Updates:

New Section: "HTTP Caching with ETags"

## HTTP Caching with ETags

The TypedCSAPINavigator supports automatic HTTP caching using ETags and conditional requests (RFC 7232). This significantly reduces bandwidth usage and improves performance for frequently accessed resources.

### How It Works

When you fetch a resource, the library:
1. Sends an `If-None-Match` header with the cached ETag (if available)
2. Receives `304 Not Modified` if the resource hasn't changed
3. Returns cached data without re-parsing
4. Stores new ETag if resource changed

### Configuration

```typescript
import { TypedCSAPINavigator } from '@camptocamp/ogc-client';

// Default: Cache Systems, Deployments, Procedures, etc. for 5 minutes
const nav = new TypedCSAPINavigator(collection);

// Custom configuration
const nav = new TypedCSAPINavigator(collection, {
  maxSize: 5000,              // Store up to 5000 entries
  ttl: 15 * 60 * 1000,       // 15 minute TTL
  enableFor: ['systems'],     // Only cache Systems
});

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

Cache Management

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

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

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

// Clear entire cache
nav.clearAllCache();

// Get cache statistics
const stats = nav.getCacheStats();
console.log(`Cache size: ${stats.size}/${stats.maxSize}`);

Best Practices

What to Cache:

  • ✅ Systems, Deployments, Procedures (stable, high hit rate)
  • ✅ Sampling Features, Properties (moderately stable)
  • ⚠️ Datastreams, Control Streams (if server provides ETags)
  • ❌ Observations, Commands, System Events (real-time, low hit rate)

When to Bypass Cache:

  • After creating/updating a resource
  • When data freshness is critical
  • When debugging cache issues

When to Clear Cache:

  • After batch updates
  • On application logout
  • On error recovery

Performance Impact

With ETags enabled (typical polling scenario):

  • Bandwidth: 92% reduction (3.5 MB → 275 KB per hour)
  • Server Load: 97% reduction (360 → 10 serializations per hour)
  • Latency: 73% faster (72s → 19.5s per hour)

---

## Acceptance Criteria

### Cache Storage (8 criteria)
- [ ] Implemented `HTTPCache` class with Map-based storage
- [ ] Support configurable max size with LRU eviction
- [ ] Support configurable TTL (time-to-live)
- [ ] Support configurable resource type filtering (`enableFor`)
- [ ] Implement `get(url)` to retrieve cached entries
- [ ] Implement `set(url, etag, data, contentType)` to store entries
- [ ] Implement `delete(url)` to remove specific entry
- [ ] Implement `clear()` to remove all entries

### ETag Extraction and Storage (5 criteria)
- [ ] Extract ETag from `ETag` response header on 200 responses
- [ ] Store ETag in cache with URL as key
- [ ] Store response data in cache for 304 handling
- [ ] Store Content-Type header in cache
- [ ] Store timestamp for TTL validation

### If-None-Match Header (5 criteria)
- [ ] Add `If-None-Match` header on requests when cache entry exists
- [ ] Use stored ETag value in `If-None-Match` header
- [ ] Don't add `If-None-Match` when `bypassCache: true`
- [ ] Don't add `If-None-Match` when no cache entry exists
- [ ] Don't add `If-None-Match` when cache entry expired (TTL)

### 304 Not Modified Handling (6 criteria)
- [ ] Detect 304 response status
- [ ] Return cached data on 304 without re-parsing
- [ ] Construct synthetic 200 response with cached data
- [ ] Set `X-Cache: HIT` header on synthetic response
- [ ] Update cache timestamp on 304 (refresh TTL)
- [ ] Handle 304 without cached data gracefully (re-fetch)

### 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)
- [ ] Implement `clearCache(resourceType, resourceId)` public method
- [ ] Implement `clearCacheForResourceType(resourceType)` public method
- [ ] Implement `clearAllCache()` public method
- [ ] Implement `getCacheStats()` public method

### Configuration (6 criteria)
- [ ] Default cache options: maxSize=1000, ttl=5min, enableFor=[stable resources]
- [ ] Accept `CacheOptions` in TypedCSAPINavigator constructor
- [ ] Support custom max size configuration
- [ ] Support custom TTL configuration
- [ ] Support custom resource type filtering
- [ ] Support disabling cache entirely (`enableFor: []`)

### TypedFetchOptions Extension (2 criteria)
- [ ] Add `bypassCache?: boolean` option to TypedFetchOptions
- [ ] Respect `bypassCache` flag in `_fetch()` method

### Testing (30 criteria)
- [ ] **Cache Storage Tests (8 tests)**:
  - Store and retrieve entries
  - Max size enforcement with LRU eviction
  - TTL expiration
  - Delete specific entry
  - Clear all entries
  - Get cache statistics
  - Multiple entries with different URLs
  - Concurrent access (edge case)

- [ ] **ETag Request Tests (6 tests)**:
  - Send If-None-Match on cache hit
  - Don't send If-None-Match on cache miss
  - Don't send If-None-Match when bypassCache=true
  - Extract ETag from 200 response
  - Store ETag in cache
  - Update ETag on subsequent 200

- [ ] **304 Not Modified Tests (5 tests)**:
  - Return cached data on 304
  - Don't parse on 304 (use cached data)
  - Set X-Cache: HIT header
  - Handle 304 without cached data
  - Verify bandwidth savings (mock verification)

- [ ] **Cache Invalidation Tests (6 tests)**:
  - Invalidate after create/update/patch/delete
  - clearCache() removes specific entry
  - clearCacheForResourceType() removes type-specific entries
  - clearAllCache() removes all entries
  - Verify cache miss after invalidation
  - Verify fresh fetch after invalidation

- [ ] **Integration Tests (5 tests)**:
  - End-to-end: Fetch → Cache → 304 → Cached data
  - End-to-end: Fetch → Update → Invalidate → Fresh fetch
  - Multiple resources with different ETags
  - Cache hit rate tracking
  - Performance comparison (time/bandwidth with vs without)

### Documentation (6 criteria)
- [ ] Add "HTTP Caching with ETags" section to README.md
- [ ] Document cache configuration options
- [ ] Document cache management methods
- [ ] Document best practices (what/when to cache)
- [ ] Document performance impact (bandwidth/latency savings)
- [ ] Add code examples for common scenarios

### Performance Validation (4 criteria)
- [ ] Verify bandwidth reduction (>80% for stable resources)
- [ ] Verify server load reduction (>90% for stable resources)
- [ ] Verify latency improvement (>50% for cached responses)
- [ ] Benchmark cache overhead (<5% for cache misses)

## Implementation Notes

### Files to Create

**New File: `src/ogc-api/csapi/http-cache.ts` (~150-200 lines)**
- `HTTPCache` class
- `CacheEntry` interface
- `CacheOptions` interface
- LRU eviction logic
- TTL validation logic

**New File: `src/ogc-api/csapi/http-cache.spec.ts` (~300-400 lines)**
- Cache storage tests
- TTL tests
- LRU eviction tests
- Statistics tests

### Files to Modify

**Modify: `src/ogc-api/csapi/typed-navigator.ts` (~50-80 lines added)**
- Add `httpCache` property
- Update constructor to accept `CacheOptions`
- Modify `_fetch()` for If-None-Match and 304 handling
- Add cache management methods (clearCache, etc.)
- Update all CRUD methods for cache invalidation

**Modify: `src/ogc-api/csapi/typed-navigator.spec.ts` (~200-300 lines added)**
- ETag request tests
- 304 handling tests
- Cache invalidation tests
- Integration tests

**Modify: `README.md` (~100-150 lines added)**
- New "HTTP Caching with ETags" section
- Configuration examples
- Cache management examples
- Best practices
- Performance impact data

### Implementation Phases

**Phase 1: Cache Infrastructure (HTTPCache class)**
- Implement HTTPCache with Map storage
- LRU eviction
- TTL expiration
- Basic tests
- **Estimated effort:** 4-6 hours

**Phase 2: ETag Integration (_fetch() method)**
- Extract ETag from responses
- Store in cache
- Add If-None-Match header
- Handle 304 responses
- **Estimated effort:** 4-6 hours

**Phase 3: Cache Invalidation**
- Update CRUD methods
- Add clearCache methods
- Add getCacheStats
- **Estimated effort:** 3-4 hours

**Phase 4: Testing**
- Write 30 comprehensive tests
- Integration tests
- Performance validation
- **Estimated effort:** 6-8 hours

**Phase 5: Documentation**
- README updates
- Code examples
- Best practices
- **Estimated effort:** 2-3 hours

**Total Estimated Effort:** 19-27 hours

### Dependencies

**Requires:**
- Issue #13 (CSAPINavigator) - ✅ COMPLETE (2,091 lines, 274 tests)
- Issue #16 (TypedCSAPINavigator) - ✅ COMPLETE (320 lines, 26 tests)

**No Blockers** - Can start immediately

### Caveats

**Server Support Required:**
- Server **MUST** provide `ETag` headers in responses
- Server **MUST** support `If-None-Match` conditional requests
- Server **MUST** return `304 Not Modified` for unchanged resources
- If server doesn't support ETags, cache layer is harmless (no-op)

**Cache Consistency:**
- Cache is **client-side only** (not shared across instances)
- Cache is **in-memory** (lost on page refresh)
- For persistent cache, consider localStorage integration (separate work item)
- For shared cache, consider service worker (separate work item)

**Memory Usage:**
- Default max size: 1000 entries
- Typical entry size: 10-50 KB (depends on resource)
- Max memory: ~10-50 MB (acceptable for modern browsers)
- Consider reducing maxSize for embedded/mobile

**Resource-Specific Behavior:**
- Systems, Deployments, Procedures: **High cache hit rate** (stable)
- Observations, Commands: **Low cache hit rate** (real-time) - don't cache
- Balance cache size vs hit rate for optimal performance

### Testing Requirements

**Unit Tests:**
- Mock fetch to simulate 200/304 responses
- Mock ETag headers
- Test cache storage and retrieval
- Test TTL expiration
- Test LRU eviction

**Integration Tests:**
- Test end-to-end flow with real parsers
- Test cache invalidation after mutations
- Test multiple concurrent requests

**Performance Tests:**
- Benchmark cache overhead on cache misses (<5%)
- Benchmark bandwidth savings on cache hits (>80%)
- Benchmark latency improvement (>50%)

**Manual Testing:**
- Test with live OGC CSAPI server
- Verify ETag headers present in responses
- Verify 304 responses received
- Measure actual bandwidth savings

## Priority Justification

**Priority: Low**

**Why Low Priority:**
1. **Not a Functional Gap**: Library works correctly without caching
2. **Performance Optimization**: Enhancement, not bug fix
3. **Server Dependency**: Requires server ETag support (not universal)
4. **Moderate Effort**: 19-27 hours of implementation + testing
5. **Advanced Feature**: Most users won't need to configure caching

**Why Still Important:**
1. **Bandwidth Efficiency**: 92% reduction for stable resources (significant for mobile)
2. **Server Load**: 97% reduction in unnecessary serializations
3. **User Experience**: 73% faster responses for cached resources
4. **Production Readiness**: Essential for high-traffic applications
5. **OGC Best Practice**: Aligns with OGC API caching recommendations

**Impact if Not Addressed:**
- ⚠️ Higher bandwidth usage (wasteful for stable resources)
- ⚠️ Higher server load (unnecessary CPU/database queries)
- ⚠️ Slower response times (unnecessary data transfer)
- ⚠️ Higher costs (metered connections, server resources)
- ✅ **Library still functional** (not a blocker)

**When to Prioritize Higher:**
- Production deployment with high traffic
- Mobile/embedded applications with bandwidth constraints
- Server experiencing high load
- User complaints about slow response times
- Metered connections with cost concerns

**Effort Estimate:** 19-27 hours
- Cache infrastructure: 4-6 hours
- ETag integration: 4-6 hours
- Cache invalidation: 3-4 hours
- Testing: 6-8 hours
- Documentation: 2-3 hours

**ROI Analysis:**
- **High ROI** for applications polling stable resources frequently
- **Moderate ROI** for typical usage patterns
- **Low ROI** for applications primarily using real-time data (Observations/Commands)

**Recommendation:** Implement after higher-priority documentation fixes and validation enhancements (work items #1-25) are complete.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions