fix: date repositories from their state #23
@@ -18,6 +18,7 @@
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Render every avatar at one consistent size, where a surrounding border had shrunk pictures by two pixels and the pixel avatar ignored an explicit size
|
- Render every avatar at one consistent size, where a surrounding border had shrunk pictures by two pixels and the pixel avatar ignored an explicit size
|
||||||
|
- Date repositories from their repository state event, so the explore list and open repository views show the latest push instead of the announcement date
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|||||||
@@ -1229,7 +1229,7 @@ impl Backend {
|
|||||||
self.sync_progress = Some((0, 0));
|
self.sync_progress = Some((0, 0));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
let progress_task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let mut last_percent: u64 = 0;
|
let mut last_percent: u64 = 0;
|
||||||
|
|
||||||
while rx.changed().await.is_ok() {
|
while rx.changed().await.is_ok() {
|
||||||
@@ -1254,23 +1254,18 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok::<(), anyhow::Error>(())
|
||||||
});
|
})
|
||||||
progress_task.detach();
|
.detach();
|
||||||
|
|
||||||
let sync = cx.background_spawn(async move {
|
let sync = cx.background_spawn(async move {
|
||||||
let opts = SyncOptions::default().progress(tx);
|
let opts = SyncOptions::default().progress(tx);
|
||||||
sync_bootstrap_only(&client, filter, opts).await
|
sync_bootstrap_only(&client, filter, opts).await
|
||||||
});
|
});
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
match sync.await {
|
match sync.await {
|
||||||
Ok(summary) => {
|
Ok(_) => {
|
||||||
log::debug!(
|
|
||||||
"sync done: {} received, {} sent",
|
|
||||||
summary.received.len(),
|
|
||||||
summary.sent.len()
|
|
||||||
);
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.sync_progress = None;
|
this.sync_progress = None;
|
||||||
cx.emit(BackendEvent::Synced);
|
cx.emit(BackendEvent::Synced);
|
||||||
@@ -1284,9 +1279,10 @@ impl Backend {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
});
|
Ok::<(), anyhow::Error>(())
|
||||||
task.detach();
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emit [`BackendEvent::Published`] for cross-store invalidation.
|
/// Emit [`BackendEvent::Published`] for cross-store invalidation.
|
||||||
|
|||||||
@@ -135,8 +135,6 @@ impl RepoListStore {
|
|||||||
|
|
||||||
backend.update(cx, |backend, cx| {
|
backend.update(cx, |backend, cx| {
|
||||||
backend.sync_bootstrap(filters::all_announcements(), 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);
|
backend.sync_bootstrap(filters::all_states(), cx);
|
||||||
// Deletion requests, NIP-09/62, must be known before any announcement is shown.
|
// Deletion requests, NIP-09/62, must be known before any announcement is shown.
|
||||||
backend.sync_bootstrap(filters::deletions(), cx);
|
backend.sync_bootstrap(filters::deletions(), cx);
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
# 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. — implemented.**
|
|
||||||
`RepoListStore` fetches each own repository's state event from the relays that
|
|
||||||
repository announces, once the list discovers it. Driving this from the list rather
|
|
||||||
than `sync_inbox` sidesteps the login race described above: whenever the announcement
|
|
||||||
list is (re)loaded, any own repository not yet fetched is covered. The fetched-set is
|
|
||||||
cleared on `SignerChanged`, so a login or account switch re-fetches.
|
|
||||||
|
|
||||||
- **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 + B + C are implemented.
|
|
||||||
Reference in New Issue
Block a user