Skip to content

Implement ETags and conditional requests for HTTP caching #62

@Sam-Bolling

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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions