-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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-Matchheaders sent on subsequent requests - No handling of
304 Not Modifiedresponses - 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 sufficeWith 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:
- Initialization Layer: Analyzes OGC API collection metadata
- Resource Layer: CRUD methods for 10 resource types
- Query Parameter Layer: Spatial, temporal, semantic, structural, pagination, projection
- Helper Method Layer: Resource availability checking, URL construction, parameter serialization
- 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:
- Automatic HTTP fetching
- Response parsing with format detection
- Type-safe return values
- Built-in validation options
- 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: 0Subsequent 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
ETagheaders in responses to enable efficient caching via conditional requests (HTTP 304).Clients SHOULD cache resources and use
If-None-Matchheaders 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.