diff --git a/crates/signed_core/src/filters.rs b/crates/signed_core/src/filters.rs index 829a0ab..678cf50 100644 --- a/crates/signed_core/src/filters.rs +++ b/crates/signed_core/src/filters.rs @@ -184,6 +184,11 @@ pub fn all_announcements() -> Filter { Filter::new().kind(Kind::GitRepoAnnouncement) } +/// All repository state events, carrying each repository's refs and last push time. +pub fn all_states() -> Filter { + Filter::new().kind(Kind::RepoState) +} + /// How far back deletion requests are fetched and stored. const DELETIONS_LOOKBACK: Duration = Duration::from_secs(3 * 365 * 86_400); diff --git a/crates/signed_state/src/repo.rs b/crates/signed_state/src/repo.rs index 05c9a86..82b03da 100644 --- a/crates/signed_state/src/repo.rs +++ b/crates/signed_state/src/repo.rs @@ -207,7 +207,11 @@ impl RepoStore { update.kind == Kind::EventDeletion || update.kind == Kind::RequestToVanish; let coordinate = update.coordinate.as_ref() == Some(addr); let author = update.author == addr.public_key; - let kind = update.kind == Kind::GitRepoAnnouncement; + // Announcements and state share the owner and identifier. + // State events carry `d`, not `a`, so `coordinate` misses them. + let authored = (update.kind == Kind::GitRepoAnnouncement + || update.kind == Kind::RepoState) + && author; // NIP-22 comments carry no `a` tag. // Coordinate matching fails for them. // Any comment may reference this repository's roots. @@ -216,18 +220,22 @@ impl RepoStore { // Any status event may reference a root of this repository. let status = RepoStatus::from_kind(update.kind).is_some(); - deletion || coordinate || (author && kind) || comment || status + deletion || coordinate || authored || comment || status }), BackendEvent::Published(event) => { - let kind = event.kind == Kind::GitRepoAnnouncement; + let announcement = event.kind == Kind::GitRepoAnnouncement; let author = event.pubkey == addr.public_key; let coordinate = event.tags.coordinates().into_iter().any(|c| c == *addr); + // State events carry their repository id in the `d` tag. + let state = event.kind == Kind::RepoState + && author + && event.tags.identifier().as_deref() == Some(addr.identifier.as_str()); // Locally published deletions may target any event of this repository. // Refresh so they take effect immediately, like relay deletions. let deletion = event.kind == Kind::EventDeletion || event.kind == Kind::RequestToVanish; - coordinate || (kind && author) || deletion + coordinate || (announcement && author) || state || deletion } _ => false, }; diff --git a/crates/signed_state/src/repos.rs b/crates/signed_state/src/repos.rs index 3c4758f..df35679 100644 --- a/crates/signed_state/src/repos.rs +++ b/crates/signed_state/src/repos.rs @@ -83,12 +83,16 @@ impl RepoListStore { BackendEvent::Published(event) => { let announcement = event.kind == Kind::GitRepoAnnouncement; + // Locally published state events advance last activity. + // Refresh so a push made in this session dates the card immediately. + let state = event.kind == Kind::RepoState; + // Locally published deletions are already in the local database. // Refresh so they take effect immediately, like relay deletions. let deletion = event.kind == Kind::EventDeletion || event.kind == Kind::RequestToVanish; - announcement || deletion + announcement || state || deletion } // Only a completed sync refreshes the list. // Progress ticks would re-scan the whole database several times @@ -133,6 +137,9 @@ impl RepoListStore { backend.update(cx, |backend, cx| { backend.sync_bootstrap(filters::all_announcements(), cx); + // State events carry the last push time, so syncing them globally + // lets the list date every repository without the user opening it. + backend.sync_bootstrap(filters::all_states(), cx); // Deletion requests, NIP-09/62, must be known before any announcement is shown. backend.sync_bootstrap(filters::deletions(), cx); }); diff --git a/docs/repo-updated-timestamp.md b/docs/repo-updated-timestamp.md index 84ad85e..9d1799e 100644 --- a/docs/repo-updated-timestamp.md +++ b/docs/repo-updated-timestamp.md @@ -123,24 +123,26 @@ flowchart TD ## Remediation options -- **A. Global state sync (smallest change).** - Add `sync_bootstrap(Filter::new().kind(Kind::RepoState))` alongside the announcement - sync in `RepoListStore::subscribe_remote`. Kind `30618` is addressable, so relays - keep one event per repository — volume comparable to announcements. Caveat: coverage - depends on whether state events reach the bootstrap relays; if a grasp server is the - only holder, those repositories remain unseen. +- **A. Global state sync (smallest change). — implemented.** + `RepoListStore::subscribe_remote` now also syncs `filters::all_states()` + (`Filter::new().kind(Kind::RepoState)`) alongside the announcement sync. Kind `30618` + is addressable, so relays keep one event per repository — volume comparable to + announcements. Caveat: coverage depends on whether state events reach the bootstrap + relays; if a grasp server is the only holder, those repositories remain unseen. -- **B. Targeted fetch for the user's own repositories.** +- **B. Targeted fetch for the user's own repositories. — not implemented.** In `sync_inbox` (`crates/signed_state/src/backend.rs` L1126-1152), where the user's own repositories' relays are already connected, also sync `filters::state(addr)` per own repository. This directly addresses "my repositories updated elsewhere" at low cost. Note the existing race: `sync_inbox` reads the announcement list right after login, which may not be loaded yet. -- **C. Reactivity fixes.** - Include `Kind::RepoState` in the `Published` refresh conditions, and in - `RepoStore::subscribe_backend` either match kind plus author, or extend `Update` to - carry the `d`-tag identifier. +- **C. Reactivity fixes. — implemented.** + `Kind::RepoState` is now included in the `BackendEvent::Published` refresh condition of + both `RepoListStore` and `RepoStore`. `RepoStore::subscribe_backend` also matches relay + state updates by owner plus kind — state events carry `d`, not `a`, so coordinate + matching misses them — and locally published state updates by owner plus the `d` + identifier. -Suggested combination: A + C, plus B if own-repository freshness on fresh profiles +A + C are implemented; B remains available if own-repository freshness on fresh profiles matters.