Commit Graph

2 Commits

Author SHA1 Message Date
Daniel Rouleau
a9aa1f73b0 Enable incremental artifact reads on every relay (#8975)
## Why

Compatible clients need incremental artifact reads for local collections
on every Buzz relay.

## What

Expose incremental artifact reads and capability discovery on every Buzz
relay.

## How

Serve incremental queries through the existing authentication and
authorization checks. Keep the existing work budgets and the separate
WebSocket sync capability. The HTTP test verifies capability discovery
and a successful incremental query.

## Risk

The relay starts no additional background work. Clients that use
incremental reads consume database capacity. The net database load is
not yet measured. Serving writers must maintain the modification
timestamps.

## Testing

Read-only SQL checks on November 3, 2026 verify the prerequisites below.
Each database records successful migrations 76–78. All five required
columns have the expected types, defaults, and nullability. All five
indexes have the expected definitions and are valid, ready, and live.

| Deployment | Schema and indexes | Running timestamp writers |
| --- | --- | --- |
| Production `bb-block` | Pass | `f6f6d9a` includes PR 8523 |
| Production `bb-public` | Pass | `f3a7bab` includes PR 8523 |
| Staging `bb-block` | Pass | `b4509de` includes PR 8523 |

The checked columns are `artifact_heads.modified_at`,
`artifact_revisions.modified_at`,
`artifact_revisions.source_channel_id`,
`channel_members.sync_modified_at`, and
`communities.artifact_policy_modified_at`. The checked indexes are
`idx_artifact_revisions_artifact`, `artifact_heads_modified`,
`artifact_heads_channel`, `channel_members_sync`, and
`artifact_revisions_moves`.

At commit `62b26ad`, all 21 NIP-11 tests and the HTTP capability test
pass on Linux with PostgreSQL 17 and Redis. The HTTP test checks
capability discovery and a successful incremental query. All required
push hooks also pass.

No manual testing. Human confirmation remains pending.

## Bigger picture

[Client PR 1507](https://github.com/block/buzz-app/pull/1507) uses the
advertised capability for local artifact sync. Existing deployments
receive this behavior when they deploy the relay version that includes
this PR.

Generated with Codex

---------

Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
2026-11-03 10:58:24 -08:00
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