From bd2cdf0923aa1c3bd2525ab748609e1922a6a844 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Wed, 21 Oct 2026 19:48:23 -0400 Subject: [PATCH] refactor(nip-ar): request sync as a REQ with artifact current A sync is the current-state filter of /query sent on a REQ. The REQ transport already means stored set, EOSE, then live, so the filter needs no separate "sync" value. Other artifact views stay HTTP-only. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson --- crates/buzz-core/src/artifact_sync.rs | 29 ++++++++++--------- .../handlers/artifact_sync_postgres_tests.rs | 2 +- crates/buzz-relay/src/protocol.rs | 12 ++++---- docs/nips/NIP-AR.md | 6 ++-- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/crates/buzz-core/src/artifact_sync.rs b/crates/buzz-core/src/artifact_sync.rs index c7e95f496c..f59a17dbc7 100644 --- a/crates/buzz-core/src/artifact_sync.rs +++ b/crates/buzz-core/src/artifact_sync.rs @@ -1,4 +1,6 @@ -//! NIP-AR sync filter: `{"artifact":"sync", "#h":[...], ...}` on a `REQ`. +//! NIP-AR sync filter: `{"artifact":"current", "#h":[...], ...}` on a `REQ`. +//! The same current-state filter as HTTP `/query`; the `REQ` transport makes +//! it a sync (complete set, `EOSE`, then live heads). use crate::artifact::{canonical_uuid, parse_predicates}; use uuid::Uuid; @@ -16,9 +18,10 @@ pub struct ArtifactSyncFilter { pub tags: Vec<(String, Vec)>, } -/// Whether a raw filter asks for sync rather than a one-shot artifact view. +/// Whether a raw `REQ` filter is a sync: the current-state view. Other +/// artifact views have no live form and stay HTTP-only. pub fn is_sync_filter(value: &serde_json::Value) -> bool { - value.get("artifact").and_then(|v| v.as_str()) == Some("sync") + value.get("artifact").and_then(|v| v.as_str()) == Some("current") } /// Parse a sync filter. It requires `#h` with canonical channel UUIDs and @@ -29,7 +32,7 @@ pub fn parse_sync(value: &serde_json::Value) -> Result) { const PRIVATE: &[&[&str]] = &[&["visibility", "private"]]; fn sync_filter(channels: &[Uuid]) -> Value { - json!({"artifact": "sync", "#h": channels, "#type": ["buzz.task"]}) + json!({"artifact": "current", "#h": channels, "#type": ["buzz.task"]}) } /// Rules 2–4 over the real pipeline: the initial set in `d` order, `EOSE`, diff --git a/crates/buzz-relay/src/protocol.rs b/crates/buzz-relay/src/protocol.rs index 2f6beafa1f..58c1a188a9 100644 --- a/crates/buzz-relay/src/protocol.rs +++ b/crates/buzz-relay/src/protocol.rs @@ -25,7 +25,7 @@ pub enum ClientMessage { /// Optional per-filter composite cursor tiebreaks from raw extension fields. before_ids: Vec>>, }, - /// A REQ whose single filter is a NIP-AR sync (`"artifact":"sync"`). + /// A REQ whose single filter is a NIP-AR sync (`"artifact":"current"`). /// An invalid sync filter is answered with `CLOSED`, so the parse error /// travels to the handler. ArtifactSync { @@ -47,8 +47,8 @@ pub enum ClientMessage { Auth(Event), } -/// Artifact queries are HTTP-only; reject rather than silently drop their -/// predicates on the generic WebSocket path. +/// Artifact views other than `current` (a sync on `REQ`) are HTTP-only; +/// reject rather than silently drop their predicates on the generic path. fn reject_artifact_query_filters(filters: &[serde_json::Value]) -> Result<()> { use buzz_core::artifact::{route_filter, FilterRoute}; for filter in filters { @@ -56,7 +56,7 @@ fn reject_artifact_query_filters(filters: &[serde_json::Value]) -> Result<()> { FilterRoute::Generic => {} FilterRoute::Artifact => { return Err(RelayError::InvalidMessage( - "artifact queries require HTTP /query or /count".into(), + "only artifact \"current\" has a REQ form; other views require HTTP /query or /count".into(), )) } FilterRoute::Rejected(reason) => return Err(RelayError::InvalidMessage(reason.into())), @@ -328,7 +328,7 @@ mod tests { #[test] fn sync_filters_route_to_artifact_sync() { let h = uuid::Uuid::new_v4(); - let sync = format!(r##"{{"artifact":"sync","#h":["{h}"]}}"##); + let sync = format!(r##"{{"artifact":"current","#h":["{h}"]}}"##); let parsed = super::ClientMessage::parse(&format!(r#"["REQ","s",{sync}]"#)).unwrap(); assert!(matches!( parsed, @@ -336,7 +336,7 @@ mod tests { )); for request in [ format!(r#"["REQ","s",{sync},{{"kinds":[1]}}]"#), - r#"["REQ","s",{"artifact":"sync"}]"#.to_owned(), + r#"["REQ","s",{"artifact":"current"}]"#.to_owned(), ] { assert!( matches!( diff --git a/docs/nips/NIP-AR.md b/docs/nips/NIP-AR.md index 76d7eea4a3..f16e14152b 100644 --- a/docs/nips/NIP-AR.md +++ b/docs/nips/NIP-AR.md @@ -111,7 +111,7 @@ Sync is additive. A client that does not use it keeps working unchanged, and a r sequenceDiagram participant C as Client participant R as Relay - C->>R: REQ sub {"artifact":"sync", "#h":[...], "#type":[...]} + C->>R: REQ sub {"artifact":"current", "#h":[...], "#type":[...]} Note over R: Register live interest first,
then read all matching heads
in one snapshot R-->>C: EVENT sub head (0..n) R-->>C: EOSE sub (the set is complete at one cut) @@ -124,10 +124,10 @@ sequenceDiagram ### Sync subscription -**Request.** Sync is a NIP-01 `REQ` whose single filter has `"artifact": "sync"`: +**Request.** Sync is a NIP-01 `REQ` whose single filter has `"artifact": "current"`. It is the current-state filter of HTTP `/query`; the `REQ` transport adds the complete set, `EOSE` and live heads. The client does not page: the relay streams the whole set until `EOSE`. Other views (`history`, `head`) have no `REQ` form and stay HTTP-only: ```json -["REQ", "s1", {"artifact": "sync", "#h": [""], "#type": ["buzz.task"]}] +["REQ", "s1", {"artifact": "current", "#h": [""], "#type": ["buzz.task"]}] ``` The filter MUST include `#h` with 1 or more channels. It accepts the same `#name` predicates and limits as a NIP-AR current-state query, with the same AND/OR rules, and also `kinds: [45010]`. It accepts no `limit` or `offset`. A relay rejects a filter without `#h`, or with an unknown key, with `CLOSED`.