This commit is contained in:
2026-01-01 16:03:37 -05:00
parent 8df1aa0c03
commit 9734e0a540

View File

@@ -0,0 +1,179 @@
# Sprint 18.1: Integration Cache Resilience
## Summary
The client caches integration IDs in localStorage but has no mechanism to handle stale IDs when they no longer exist on the server. This causes infinite retry loops, confusing error states, and cross-device inconsistencies.
## Problem Statement
**Scenario:** Client has a cached integration ID that no longer exists in the backend database (e.g., deleted, workspace mismatch, or cross-device sync).
**Current Behavior:**
1. Client caches integration ID in localStorage
2. Server returns "Integration not found" (gRPC `NOT_FOUND`)
3. Client catches error generically, shows toast
4. Cache is NOT cleared
5. Client retries with stale ID → infinite error loop
## Risk Assessment
| Aspect | Status | Severity |
|--------|--------|----------|
| Cache invalidation on "Not Found" | ❌ Missing | **CRITICAL** |
| Server validation at startup | ❌ Missing | **HIGH** |
| Retry logic for stale IDs | ⚠️ Retries indefinitely | **HIGH** |
| Error differentiation | ⚠️ All errors treated same | **MEDIUM** |
| Workspace validation | ❌ Client ignores workspace_id | **MEDIUM** |
| Cross-device consistency | ❌ No sync mechanism | **MEDIUM** |
## Root Cause Analysis
### 1. Client-Side Caching Locations
| Location | What's Cached | Persistence |
|----------|---------------|-------------|
| `preferences.integrations` | Full `Integration` objects | localStorage |
| `useIntegrationSync` hook | `syncStates` by ID | In-memory |
| `useOAuthFlow` hook | Connection status only | React state |
### 2. Missing Error Handling
**Calendar Service** (`calendar_service.py`):
- Returns generic "Provider X not connected" for missing integrations
- No explicit "integration not found" error code
**Rust Client** (`sync.rs`, `calendar.rs`):
- Uses `?` operator, propagates all errors identically
- No cache invalidation on failures
**TypeScript Hooks** (`use-integration-sync.ts`, `use-calendar-sync.ts`):
- Catch errors generically
- Never clear cached integration on "Not Found"
- Continue polling/retrying indefinitely
### 3. Missing Startup Validation
- `preferences.initialize()` hydrates from Tauri storage blindly
- No RPC to validate cached integration IDs against server
- `useIntegrationSync.startScheduler()` assumes cache is accurate
## Execution Checklist
### Phase 1: Error Differentiation
- [ ] Define `IntegrationNotFoundError` in TypeScript (`client/src/api/errors.ts`)
- [ ] Update Rust client to extract gRPC status codes and forward to TypeScript
- [ ] Update `extractErrorMessage()` helper to preserve error type/code
### Phase 2: Cache Invalidation on "Not Found"
- [ ] `useIntegrationSync`: Clear integration from cache on `NOT_FOUND`
- [ ] `useCalendarSync`: Clear provider on `NOT_FOUND`
- [ ] `useOAuthFlow`: Reset connection state on `NOT_FOUND`
- [ ] Add `preferences.removeIntegration(id)` call in error handlers
### Phase 3: Startup Validation
- [ ] Add `GetUserIntegrations` RPC to fetch server's authoritative list
- [ ] Implement `validateCachedIntegrations()` in preferences module
- [ ] Call validation during `preferences.initialize()`
- [ ] Remove any cached IDs not present in server response
### Phase 4: Workspace-Aware Caching
- [ ] Store `workspace_id` with cached integrations
- [ ] Filter cached integrations by current workspace on load
- [ ] Clear integrations from other workspaces
### Phase 5: Health Check (Optional)
- [ ] Add periodic `GetOAuthConnectionStatus()` calls for connected integrations
- [ ] Auto-clear if status becomes `DISCONNECTED` unexpectedly
- [ ] Show warning in UI if stale state detected
## Files to Create
| File | Purpose |
|------|---------|
| `client/src/api/errors.ts` | Typed error classes (`IntegrationNotFoundError`, etc.) |
## Files to Modify
| File | Changes |
|------|---------|
| `client/src/hooks/use-integration-sync.ts` | Cache invalidation on NOT_FOUND |
| `client/src/hooks/use-calendar-sync.ts` | Cache invalidation on NOT_FOUND |
| `client/src/hooks/use-oauth-flow.ts` | Reset state on NOT_FOUND |
| `client/src/lib/preferences.ts` | Add `validateCachedIntegrations()`, workspace filtering |
| `client/src/api/tauri-adapter.ts` | Add `getUserIntegrations()` method |
| `client/src-tauri/src/grpc/client/sync.rs` | Extract and forward gRPC status codes |
| `src/noteflow/grpc/proto/noteflow.proto` | Add `GetUserIntegrations` RPC (optional) |
## Key Design Decisions
### Error Type Propagation
```typescript
// client/src/api/errors.ts
export class IntegrationNotFoundError extends Error {
constructor(integrationId: string) {
super(`Integration ${integrationId} not found`);
this.name = 'IntegrationNotFoundError';
}
}
// Usage in hooks
try {
await api.startIntegrationSync(integrationId);
} catch (error) {
if (error instanceof IntegrationNotFoundError) {
preferences.removeIntegration(integrationId);
return; // Stop retrying
}
// Handle retryable errors...
}
```
### Startup Validation Flow
```
App Startup
preferences.initialize()
validateCachedIntegrations()
├──► Call GetUserIntegrations RPC
Compare cached IDs vs server IDs
├──► Remove stale IDs from cache
Continue with validated cache
```
### Workspace Filtering
```typescript
// preferences.ts
function filterByWorkspace(integrations: Integration[], workspaceId: string): Integration[] {
return integrations.filter(i => i.workspace_id === workspaceId);
}
```
## Success Criteria
1. ✅ Client clears cached integration when server returns "Not Found"
2. ✅ No infinite retry loops for stale integration IDs
3. ✅ Startup validation removes orphaned integrations
4. ✅ Workspace switch clears other workspace's integrations
5. ✅ User sees clear error message with action to reconnect
## Related Issues
- Workspace ID mismatch bug (fixed in identity.rs, fixtures.ts, calendar_service.py)
- OAuth browser popup fix (use-oauth-flow.ts shell plugin)