Archive sync flush re-derives identity from current AppState, dropping in-flight events across an identity/community switch
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
`AppIo::archive` re-reads identity and relay from the current global `AppState` at flush time, rather than using the scope the archive sync task was started with. An identity or community switch between "events buffered" and "buffer flushed" validates scope-A events under scope-B credentials, and the backend drops them.
Found by review (Carl) on #6024; confirmed against source and scoped there before deferring here.
## Mechanism
`archive/sync.rs:294-302`:
```rust
fn archive(&self, candidates: Vec) -> BoxFuture<'_, Result> {
Box::pin(async move {
let state: State<'_, AppState> = self.app.state();
super::archive_candidates(&state, candidates).await
})
}
```
`archive_candidates` (`archive/mod.rs:166-167`) then derives `identity_pubkey(state)` and `relay_ws_url_with_override(state)` from whatever is current *now*. `run_sync` flushes its buffered batch after `cancel` fires (`sync.rs:233`), which is exactly when a switch has already landed.
Meanwhile `start_archive_sync` already captures the scope it was started under (`sync.rs:505`) — it just isn't threaded into `AppIo`.
## Not a regression
Base `78cbffeb6` had the same race through the renderer: `archiveSyncManager.destroy()` did `void this.deps.archiveEvents(batch)`, unawaited, into the same backend-scoped `archive_candidates`, which re-read identity the same way.
What #6024 changed is the width of the window, not its existence. The JS flush went out synchronously in the cleanup tick; the native flush happens after `cancel` propagates to a tokio task that may be parked mid-`reconcile`. Wider, same shape.
Filed separately rather than added to #6024: pre-existing, and a different subsystem from the two blockers that PR fixes.
## Suggested fix
Capture `(identity_pubkey, relay_url)` into `AppIo` at construction — `start_archive_sync` already has both in `scope` — and pass them to an `archive_candidates` variant that takes them explicitly instead of re-deriving from `AppState`. A flush that finds the scope has moved should then drop or refuse rather than silently validate under the wrong identity.
## Impact
Events are dropped, not mis-attributed: the backend re-verifies scope claims, so scope-A events fail validation under B. Data loss is bounded by one in-flight batch at switch time, and only for the ephemeral (`owner_p`) scope is it unrecoverable — persistent scopes re-query the relay.
Contributor guide
Assessment
This issue has not been assessed yet.