The explore list fell back to the announcement created_at because kind 30618 state events were only fetched per repository, on open. Sync them globally and refresh the list on published state events, so a card shows the last push without the user cloning first. Open repository panels now also react to state events from relays and to locally published state events.
149 lines
7.3 KiB
Markdown
149 lines
7.3 KiB
Markdown
# Repository card "Updated" timestamps are stale until first open
|
|
|
|
Feedback under investigation:
|
|
|
|
> My repositories that I updated yesterday show up as being updated like 6 months ago,
|
|
> but then when I open them and they get cloned that timestamp is fixed, which means
|
|
> you're not reading the repository state event, only the date of the repository
|
|
> announcement event and the git history?
|
|
|
|
## Summary
|
|
|
|
The reviewer is essentially right. The backend *has* logic to include kind `30618`
|
|
repository state events in the "last activity" timestamp, but that logic only reads
|
|
events that are **already present in the local nostr database**. The app never
|
|
globally fetches state events for repositories the user has not opened, so for those
|
|
the timestamp falls back to the announcement's `created_at` — the repository's
|
|
creation date. Opening (and cloning) the repository fetches the state event, which is
|
|
why the timestamp corrects itself afterwards.
|
|
|
|
The "git history" part of the hypothesis is not accurate for this label: no
|
|
git-derived timestamp feeds the repository card. The only git-derived times in the UI
|
|
are commit timestamps in commit lists and diffs, which are unrelated.
|
|
|
|
This is a data-coverage bug, not a missing computation. The code that aggregates state
|
|
events is correct; it is simply starved of input on the explore list.
|
|
|
|
## Where the timestamp comes from
|
|
|
|
The label is rendered in `crates/workspace/src/views/repo_list.rs` (L209-212):
|
|
|
|
```rust
|
|
let activity = last_activity
|
|
.map(relative_time)
|
|
.map(|label| SharedString::from(format!("Updated {label}")))
|
|
```
|
|
|
|
`last_activity` is computed in `crates/signed_state/src/repos.rs`,
|
|
`RepoListStore::run_refresh` (L192-235), as a max over three sources:
|
|
|
|
| Source | Code | Availability |
|
|
|---|---|---|
|
|
| `announcement.created_at` (kind 30617) | L195-198 | always; seeded for every listed repo |
|
|
| kind 30618 repository state events | L200-213 | only events already in the local database |
|
|
| NIP-34 activity (issues, PRs, patches, statuses, comments) | L215-235 | only local database, windowed to 90 days (`ACTIVITY_WINDOW`, L13-14) |
|
|
|
|
The critical gap is the global sync scope. `RepoListStore::subscribe_remote`
|
|
(`crates/signed_state/src/repos.rs` L131-139) only syncs:
|
|
|
|
```rust
|
|
backend.sync_bootstrap(filters::all_announcements(), cx); // kind 30617 only
|
|
backend.sync_bootstrap(filters::deletions(), cx);
|
|
```
|
|
|
|
State events are fetched **per repository, on open**, via `RepoStore`:
|
|
|
|
- `repo_filters` (`crates/signed_state/src/repo.rs` L254-270) queries
|
|
`[GitRepoAnnouncement, RepoState]` plus activity and deletions.
|
|
- `subscribe_remote` (L296-306) runs those filters against the bootstrap relays.
|
|
- `connect_announced_relays` (L272-294) runs a negentropy sync of the same filters
|
|
against the relays in the announcement's NIP-34 `relays` tag.
|
|
- Both fire from `RepoStore::new`'s deferred initialization (L93-112).
|
|
|
|
Publishes only ever create a state event, never re-publish the announcement:
|
|
`sign_state_event` in `crates/signed_state/src/backend.rs` (L1655-1682) and the
|
|
fan-out at L907-918. A repository created 6 months ago and pushed yesterday therefore
|
|
has an announcement dated 6 months ago and a state event dated yesterday.
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[App start / login] --> B[RepoListStore sync: 30617 announcements + deletions]
|
|
B --> C[Explore card: Updated = announcement created_at]
|
|
C --> D{Repo opened?}
|
|
D -- no --> C
|
|
D -- yes --> E[RepoStore fetches 30617 + 30618 from bootstrap + announced relays]
|
|
E --> F[State event stored in local DB]
|
|
F --> G[BackendEvent NostrUpdate RepoState]
|
|
G --> H[RepoListStore refreshes, last_activity recomputed]
|
|
H --> I[Card shows push date]
|
|
```
|
|
|
|
## Why the report reproduces exactly
|
|
|
|
1. The announcement (kind 30617) is addressable and effectively static; pushes replace
|
|
only the state event (kind 30618).
|
|
2. On a database that has never fetched that repository (fresh install, another device,
|
|
or wasm where the database is in-memory per session), only the announcement is
|
|
known, so the card shows the creation date.
|
|
3. Opening the repository triggers the per-repo fetch; the state event lands in the
|
|
database, and `RepoListStore` does react to `RepoState` updates
|
|
(`crates/signed_state/src/repos.rs` L69-82), so the card corrects itself.
|
|
|
|
## Secondary issues found in the same area
|
|
|
|
1. **`RepoStore` ignores live state events.**
|
|
`subscribe_backend` (`crates/signed_state/src/repo.rs` L195-239) matches relay
|
|
updates via `Update.coordinate`, which is built from `a` tags only
|
|
(`crates/signed_nostr/src/update.rs` L13-21). A kind `30618` event carries `d`, not
|
|
`a`, and the kind check only accepts `GitRepoAnnouncement`. A state update arriving
|
|
from a relay therefore does not refresh an open repository panel's `head`.
|
|
`RepoListStore` is unaffected because its check is kind-based.
|
|
|
|
2. **Locally published state events refresh nothing.**
|
|
`BackendEvent::Published` is ignored for `RepoState` by both stores
|
|
(`crates/signed_state/src/repos.rs` L83-92 and `crates/signed_state/src/repo.rs`
|
|
L221-231). The comment in `crates/signed_state/src/backend.rs` L907-909
|
|
("publishing notifies the repository views") is inaccurate: after an in-app push,
|
|
the explore card stays stale until the next unrelated refresh, sync or restart,
|
|
even though the event is in the local database.
|
|
|
|
3. **Usage counts are similarly under-scoped.**
|
|
The Popular ranking (`crates/signed_state/src/repos.rs` L239-261) counts only
|
|
issues, PRs and patches present locally, i.e. mostly repositories the user has
|
|
opened.
|
|
|
|
4. **The Recent tab is not "recently updated".**
|
|
`RepoFilter::Recent` (`crates/workspace/src/views/repo_list.rs` L73-84) truncates the
|
|
announcement-sorted list (`crates/signed_state/src/repos.rs` L189-190), so it orders
|
|
by creation, not by `last_activity`.
|
|
|
|
5. **The 90-day activity window is deliberate.**
|
|
Activity-driven bumps decay after `ACTIVITY_WINDOW`; only state events would bump
|
|
indefinitely once fetched (L13-14).
|
|
|
|
## Remediation options
|
|
|
|
- **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. — 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. — 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.
|
|
|
|
A + C are implemented; B remains available if own-repository freshness on fresh profiles
|
|
matters.
|