fix: reconnecting to same remote archive server fails after clearing data#110
fix: reconnecting to same remote archive server fails after clearing data#110luandro wants to merge 3 commits into
Conversation
…data
Four root causes identified and fixed:
1. clearAllData() preserved remoteServers table, leaving stale records
that blocked re-adding the same server via duplicate check.
- Now clears remoteServers and iconCache tables too.
2. StorageSettings didn't clear the in-memory auth store after clearing
IndexedDB, creating inconsistency between memory and DB.
- Added useAuthStore.getState().clearAll() call after clearAllData().
3. syncRemoteArchive() returned error on lock contention, causing
confusing failures when auto-sync and manual add competed.
- Now returns the existing in-flight Promise, waiting for it to
complete instead of rejecting.
4. No connection validation before adding a server — invalid/unreachable
servers were added then failed during sync.
- Added healthCheck + getProjects validation with specific error
messages for unreachable server and invalid token.
Closes #73
| const existingSync = activeSyncs.get(serverDbId); | ||
| if (existingSync) { | ||
| return { | ||
| success: false, | ||
| error: 'Sync already in progress', | ||
| }; | ||
| return existingSync; | ||
| } |
There was a problem hiding this comment.
Second caller's options silently dropped on concurrent sync
When a second syncRemoteArchive call for the same serverDbId arrives while a sync is in-flight, the second caller's options (including a potentially different/refreshed token) are discarded without ensureServerInStore being called for them. In the scenario where a token is rotated between two overlapping sync cycles, the new credentials are never written to the auth store — any sync that starts after the in-flight one completes will still use the stale token from the first call. The auth store will only get the updated token when a fresh (non-deduplicated) syncRemoteArchive call is made after the in-flight one settles.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/sync.ts
Line: 257-260
Comment:
**Second caller's options silently dropped on concurrent sync**
When a second `syncRemoteArchive` call for the same `serverDbId` arrives while a sync is in-flight, the second caller's `options` (including a potentially different/refreshed token) are discarded without `ensureServerInStore` being called for them. In the scenario where a token is rotated between two overlapping sync cycles, the new credentials are never written to the auth store — any sync that starts after the in-flight one completes will still use the stale token from the first call. The auth store will only get the updated token when a fresh (non-deduplicated) `syncRemoteArchive` call is made after the in-flight one settles.
How can I resolve this? If you propose a fix, please make it concise.| @@ -307,7 +361,7 @@ function AddArchiveServerDialog({ | |||
| ); | |||
There was a problem hiding this comment.
Unhandled rejection from
finalizeAddServer in encrypted invite path
The async (redeemed) => { await finalizeAddServer(...) } callback runs inside .then(onFulfilled, onRejected). If finalizeAddServer throws for any reason (e.g. an unexpected error from checkDuplicate, dispatch, or a future caller that changes the function), the rejection from the async callback is not caught by the onRejected handler — that handler only covers redeemEncryptedInvite failures. The result is an unhandled promise rejection and a dialog permanently stuck in the submitting state. Adding a .catch() at the end of the chain (or wrapping finalizeAddServer in a try/catch that dispatches an error) would close the gap.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/screens/Home/AddArchiveServerDialog.tsx
Line: 339-361
Comment:
**Unhandled rejection from `finalizeAddServer` in encrypted invite path**
The `async (redeemed) => { await finalizeAddServer(...) }` callback runs inside `.then(onFulfilled, onRejected)`. If `finalizeAddServer` throws for any reason (e.g. an unexpected error from `checkDuplicate`, `dispatch`, or a future caller that changes the function), the rejection from the async callback is not caught by the `onRejected` handler — that handler only covers `redeemEncryptedInvite` failures. The result is an unhandled promise rejection and a dialog permanently stuck in the submitting state. Adding a `.catch()` at the end of the chain (or wrapping `finalizeAddServer` in a try/catch that dispatches an error) would close the gap.
How can I resolve this? If you propose a fix, please make it concise.The AddArchiveServerDialog component (added in this PR) imports ApiError, apiClient, and redeemEncryptedInvite from @/lib/api-client. The Storybook mock for this module was missing the ApiError class and apiClient object, causing all three Storybook-related CI checks to fail: - storybook-tests: import error on HomeScreen.stories.tsx - storybook-test-runner: build-storybook failed with MISSING_EXPORT - visual-regression-check: same build-storybook failure Add the missing exports to the Storybook api-client mock.
|
PR Readiness Worker — Run 2026-06-11T18:30:22Z
|
|
Preview deployment ready: https://agent-comapeo-cloud-app-issu-kaq7.comapeo-cloud-app.pages.dev Commit: |
Description
Fixes #73
Root Causes & Fixes
Four root causes were identified and fixed:
1.
clearAllData()preserved remoteServers tableStale records blocked re-adding the same server via duplicate check.
remoteServersandiconCachetables alongside all other tables.2. In-memory auth store not cleared after IndexedDB wipe
StorageSettings cleared IndexedDB but left the auth store in memory, creating inconsistency.
useAuthStore.getState().clearAll()call afterclearAllData().3. Lock contention in syncRemoteArchive()
Concurrent sync calls returned confusing errors instead of waiting.
4. No connection validation before adding a server
Invalid/unreachable servers were added silently and failed during sync.
healthCheck+getProjectsvalidation with specific error messages.Testing
Closes #73
Greptile Summary
This PR fixes four root causes behind reconnecting to the same remote archive server failing after "Clear All Cached Data":
clearAllDatanow also wipesremoteServersandiconCache, the in-memory auth store is cleared alongside IndexedDB, concurrentsyncRemoteArchivecalls for the same server share a single in-flight Promise instead of colliding, and ahealthCheck+getProjectsvalidation step guards against silently adding unreachable or unauthorized servers.clearAllData/StorageSettings:remoteServersandiconCachetables are now cleared in the same transaction, anduseAuthStore.clearAll()is called immediately after to drain the in-memory mirror.sync.tsconcurrency guard: a module-levelactiveSyncsMap returns the existing Promise to any concurrent caller for the sameserverDbId, preventing duplicate sync attempts.AddArchiveServerDialogpre-validation:validateConnectionrunshealthCheckthengetProjects(checking for 401) before writing the server to the store, surfacing "Cannot connect" or "Invalid token" errors before the server is persisted.Confidence Score: 5/5
Safe to merge — all four targeted bugs are correctly fixed and 1105 tests pass.
The core fixes are well-scoped and consistent:
clearAllDatawipes the two previously-missed tables inside an existing transaction, the auth-store wipe pairs correctly with the DB wipe, the concurrency guard correctly deduplicates in-flight Promises with a guaranteed.finally()cleanup, and the pre-validation guards both the happy and error paths. The only notes are a minor edge case where an in-flight sync could re-populate a just-cleared database, and a floatingaddServerpromise where anonAddedthrow would leave the dialog in a loading state — neither is a regression and both are uncommon paths.No files require special attention; the two notes above are low-probability edge cases that do not affect the primary fix.
Important Files Changed
activeSyncsMap) that deduplicates in-flight syncs by returning the existing Promise; works correctly, but the Map is never cleared onclearAllData, which could let an in-flight sync re-populate a just-wiped database.db.remoteServers.clear()anddb.iconCache.clear()toclearAllData(); both tables exist in the schema and are inside the existingdb.transaction('rw', db.tables, ...)scope — change is correct.useAuthStore.getState().clearAll()afterclearAllData()to synchronise the in-memory auth store with the newly-emptied IndexedDB; consistent with howclearAllStorageworks elsewhere.validateConnection(healthCheck + getProjects) before committing a server; logic is sound. TheaddServerpromise is not awaited in eitherfinalizeAddServerorhandleAdvancedSubmit, leaving a gap where anonAddedthrow would produce an unhandled rejection and a permanently loading button.clearAllData,clearServerData, andgetStorageStats; existing structure unchanged.Sequence Diagram
sequenceDiagram participant U as User participant D as AddArchiveServerDialog participant V as validateConnection participant A as authStore participant DB as IndexedDB participant S as syncRemoteArchive U->>D: Submit (URL + token) D->>V: healthCheck(url) V-->>D: true/false alt unreachable D-->>U: Could not connect end D->>V: getProjects(url, token) V-->>D: ok / 401 alt 401 D-->>U: Invalid token end D->>A: addServer(label, url, token) A-->>D: serverId D-->>U: onAdded(serverId) U->>D: Clear All Data D->>DB: clearAllData() D->>A: clearAll() note over A,DB: In-memory and IndexedDB both empty U->>D: Re-add same server D->>S: syncRemoteArchive(serverId, options) S->>S: activeSyncs.get(serverId)? alt already syncing S-->>D: return existing Promise else new sync S->>DB: pullProjects / pullObservations DB-->>S: stored S->>A: updateServerStatus connected endPrompt To Fix All With AI
Reviews (2): Last reviewed commit: "fix(storybook): add missing ApiError and..." | Re-trigger Greptile