Files
buzz/docs
Daniel Rouleau a57349b237 feat(relay): add incremental artifact reads (#8523)
## Summary

Buzz clients need to recover task changes after a disconnect. This adds
incremental reads of changed artifacts with database modification
timestamps and a 120-second overlap. Each result contains the current
revision or an explicit removal. A separate read of known IDs confirms
accepted edits and restores access state.

Pages use a timestamp and a stable ID to handle equal timestamps. Access
grants include older records. Access policy changes require discovery
and revalidation of saved IDs. The reader does not load a list of all
readable channels. Ordinary artifact writes update their own rows.

### Incremental reads and WebSocket Sync

This HTTP protocol maintains an eventually consistent local store. It
uses `artifacts.incremental` and `"artifact":"incremental"`. It does not
prove a complete set at one database snapshot. Absence never proves
deletion or loss of access.

[WebSocket Sync](https://github.com/block/buzz/pull/8522) serves a
separate use case: a complete set at `EOSE`, followed by live updates.
It owns `artifacts.sync`. Both protocols can coexist. The second PR to
merge must reuse the shared access helper in `artifact_query.rs` when it
resolves the overlapping edits.

The gate is `BUZZ_ARTIFACT_INCREMENTAL_ENABLED`. Errors use the
`incremental_` prefix. Unlisted open channels require membership or an
explicit `#h` scope. Listing changes reset incremental progress.
Incremental reads exclude TTL artifacts. Known-ID reads return `remove`
for them. The `#h` scope limits candidates before pagination. Initial
reads omit deleted heads. Later reads retain deletion removals.

### Why the new timestamps are needed

`created_at` comes from the event signer. `received_at` records event
arrival. Neither advances when access changes or an event is redacted.
Sync needs database timestamps for those changes:

| Column | Why sync needs it |
| --- | --- |
| `artifact_heads.modified_at` | Finds current records that change
through an edit, deletion, or redaction. |
| `artifact_revisions.modified_at` | Dates retained move records. This
lets sync remove a task from its former channel when the reader cannot
access its new channel. |
| `channel_members.sync_modified_at` | Finds grants, rejoins, and
removals. A grant must include older tasks whose own timestamps do not
change. |
| `communities.artifact_policy_modified_at` | Requests recovery after a
relevant channel visibility, TTL, or deletion change. Clients revalidate
saved IDs. Ordinary task writes do not update it. |

The database sets these timestamps with `clock_timestamp()`. The
120-second overlap covers the accepted delay between timestamp
assignment and commit visibility.

`BUZZ_ARTIFACT_INCREMENTAL_ENABLED` defaults to off. Apply migrations
`0076`–`0078` and upgrade all writers before enablement. The 120-second
delay bound is an operating assumption. [Deployment
steps](https://github.com/block/buzz/blob/drouleau/artifact-snapshots/docs/artifact-sync-deployment.md)
cover index prebuilds and rollback.

### Related issue

No duplicate found. Related work: [read
permissions](https://github.com/block/buzz/pull/8338), [statement
limits](https://github.com/block/buzz/pull/8461), and [atomic channel
settings](https://github.com/block/buzz/pull/8516).

### Testing

Signed HTTP E2E passes on `f3d6ac852e7ce4e2cc53bd891cef87832a868d72`
after CI passes. It includes main through `ef4130da4`. A standalone
local relay uses a fresh PostgreSQL 17 database, Redis, and signed
NIP-98 HTTP requests. All 78 migrations apply through relay startup. All
behavioral writes and reads use HTTP.

The tests cover create, edit, delta reads, known-ID recovery, private
moves, access grants, removals, rejoins, visibility resets, redaction,
and deletion. Unlisted tests cover hidden broad discovery, named scope,
known-ID reads, member access, and policy resets. They also cover
bootstrap across two pages, overlap replay, and 101 move removals across
pages. Decoded cursors contain no private revision IDs. With the
incremental gate off, `artifacts.incremental` is absent and a signed
incremental request returns `503 incremental_unavailable`. Unsigned
requests are rejected. Signed tests also cover TTL exclusion, current-ID
removals for TTL artifacts, moves into TTL channels, deleted initial
heads, and scoped pages with exactly 100 and 3 upserts.

A separate local scale test on `babcdb766` reads 10,050 tasks across
60,002 memberships. It uses seeded fixtures and a second relay state
with the same signing key. The initial read takes 8.31 seconds over 101
pages with 4,923-byte events. A read with no changes takes 5.70
milliseconds. The candidate keeps the same database query. These are
local measurements, not production limits. Cold pages repeat candidate
scans and access checks. Writer load needs validation before fleet
enablement.

```sh
cargo test -p buzz-relay sync_downloads_ten_thousand_tasks_with_sixty_thousand_memberships -- --ignored --nocapture
```

The behavioral run uses `cargo build -p buzz-relay --bin buzz-relay`,
then `cargo run -p buzz-relay --example
artifact_incremental_final_oct30_f3`. The temporary harness drives the
built relay and is removed after the run. The worktree is clean at the
tested SHA. It does not exercise a native client. Git and S3 behavior
are outside this validation.

The
[protocol](https://github.com/block/buzz/blob/drouleau/artifact-snapshots/docs/nips/NIP-AR.md#incremental-reads)
defines progress, removals, limits, and recovery. Relay verification
covers the signed HTTP path. Native client verification belongs to [the
client PR](https://github.com/block/buzz-app/pull/1507).

Generated with Codex

---------

Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
2026-10-30 08:30:46 -07:00
..
…