mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
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 <loganj@squareup.com>
This commit is contained in:
@@ -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<String>)>,
|
||||
}
|
||||
|
||||
/// 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<ArtifactSyncFilter, &'sta
|
||||
.as_object()
|
||||
.ok_or("artifact filter must be an object")?;
|
||||
if !is_sync_filter(value) {
|
||||
return Err("artifact must be \"sync\"");
|
||||
return Err("artifact must be \"current\"");
|
||||
}
|
||||
let tags = parse_predicates(object, &["artifact", "kinds"])?;
|
||||
let mut channels = Vec::new();
|
||||
@@ -56,20 +59,20 @@ mod tests {
|
||||
fn sync_filter_requires_channels_and_rejects_paging() {
|
||||
let a = Uuid::new_v4().to_string();
|
||||
let parsed = parse_sync(
|
||||
&json!({"artifact":"sync","#h":[a, a],"#type":["buzz.task"],"kinds":[45010]}),
|
||||
&json!({"artifact":"current","#h":[a, a],"#type":["buzz.task"],"kinds":[45010]}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parsed.channels.len(), 1);
|
||||
assert_eq!(parsed.tags.len(), 2);
|
||||
for invalid in [
|
||||
json!({"artifact":"sync"}),
|
||||
json!({"artifact":"sync","#type":["buzz.task"]}),
|
||||
json!({"artifact":"sync","#h":[a],"limit":10}),
|
||||
json!({"artifact":"sync","#h":[a],"offset":0}),
|
||||
json!({"artifact":"sync","#h":[a],"kinds":[45011]}),
|
||||
json!({"artifact":"sync","#h":["not-a-uuid"]}),
|
||||
json!({"artifact":"sync","#h":[a],"since":1}),
|
||||
json!({"artifact":"current","#h":[a]}),
|
||||
json!({"artifact":"current"}),
|
||||
json!({"artifact":"current","#type":["buzz.task"]}),
|
||||
json!({"artifact":"current","#h":[a],"limit":10}),
|
||||
json!({"artifact":"current","#h":[a],"offset":0}),
|
||||
json!({"artifact":"current","#h":[a],"kinds":[45011]}),
|
||||
json!({"artifact":"current","#h":["not-a-uuid"]}),
|
||||
json!({"artifact":"current","#h":[a],"since":1}),
|
||||
json!({"artifact":"history","#h":[a]}),
|
||||
] {
|
||||
assert!(parse_sync(&invalid).is_err(), "{invalid}");
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ async fn no_frame(rx: &mut mpsc::Receiver<WsMessage>) {
|
||||
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`,
|
||||
|
||||
@@ -25,7 +25,7 @@ pub enum ClientMessage {
|
||||
/// Optional per-filter composite cursor tiebreaks from raw extension fields.
|
||||
before_ids: Vec<Option<Vec<u8>>>,
|
||||
},
|
||||
/// 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!(
|
||||
|
||||
@@ -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,<br/>then read all matching heads<br/>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": ["<channel>"], "#type": ["buzz.task"]}]
|
||||
["REQ", "s1", {"artifact": "current", "#h": ["<channel>"], "#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`.
|
||||
|
||||
Reference in New Issue
Block a user