Skip to main content

simulator_api/
subscribe_config.rs

1//! Cross-cutting subscribe options shared by the client and server.
2//!
3//! In a subscribe request's positional `[filter, config]` params, the `config`
4//! object carries per-kind fields (e.g. `commitment`) plus these options. The
5//! client builds [`SubscribeConfig`] and merges it in; the server parses it back
6//! out — one typed home for the wire field names. Absent fields default off, so
7//! peers without support interoperate.
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12/// Compression negotiated for notification frames. `lowercase` so the variant
13/// matches the on-wire token (`"zstd"`).
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum Compression {
17    Zstd,
18}
19
20/// Cross-cutting subscribe options the client sets in the config object.
21#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct SubscribeConfig {
23    /// Last slot the client already processed. On reconnect the server replays
24    /// from this slot (inclusive) rather than streaming the full history.
25    #[serde(
26        rename = "replayFromSlot",
27        default,
28        skip_serializing_if = "Option::is_none"
29    )]
30    pub replay_from_slot: Option<i64>,
31    /// Notification-frame compression the client opted into. `None` keeps the
32    /// uncompressed `Text` path.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub compression: Option<Compression>,
35}
36
37impl SubscribeConfig {
38    /// Parse the cross-cutting options out of a subscribe config object. Per-kind
39    /// fields are ignored, and a missing or malformed object yields defaults
40    /// (all off), so a subscribe never fails on these optional fields.
41    pub fn from_value(config: Option<&Value>) -> Self {
42        config
43            .cloned()
44            .and_then(|v| serde_json::from_value(v).ok())
45            .unwrap_or_default()
46    }
47
48    /// Merge the set options into a subscribe request's config object (the second
49    /// positional param), leaving its per-kind fields untouched. A no-op if the
50    /// params don't have a config object.
51    pub fn apply_to(&self, params: &mut Value) {
52        let Some(config) = params.get_mut(1).and_then(Value::as_object_mut) else {
53            return;
54        };
55        if let Ok(Value::Object(fields)) = serde_json::to_value(self) {
56            config.extend(fields);
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn parses_set_options_and_ignores_per_kind_fields() {
67        let cfg = SubscribeConfig::from_value(Some(&serde_json::json!({
68            "commitment": "confirmed",
69            "replayFromSlot": 42,
70            "compression": "zstd",
71        })));
72        assert_eq!(cfg.replay_from_slot, Some(42));
73        assert_eq!(cfg.compression, Some(Compression::Zstd));
74    }
75
76    #[test]
77    fn absent_or_malformed_options_default_off() {
78        assert_eq!(
79            SubscribeConfig::from_value(None),
80            SubscribeConfig::default()
81        );
82        let cfg =
83            SubscribeConfig::from_value(Some(&serde_json::json!({ "commitment": "confirmed" })));
84        assert_eq!(cfg, SubscribeConfig::default());
85        // An unrecognized compression token leaves compression off.
86        let cfg = SubscribeConfig::from_value(Some(&serde_json::json!({ "compression": "gzip" })));
87        assert_eq!(cfg.compression, None);
88    }
89
90    #[test]
91    fn apply_to_merges_into_config_without_clobbering() {
92        let mut params =
93            serde_json::json!([{ "mentions": ["prog"] }, { "commitment": "confirmed" }]);
94        SubscribeConfig {
95            replay_from_slot: Some(7),
96            compression: Some(Compression::Zstd),
97        }
98        .apply_to(&mut params);
99        assert_eq!(params[1]["commitment"], "confirmed");
100        assert_eq!(params[1]["replayFromSlot"], 7);
101        assert_eq!(params[1]["compression"], "zstd");
102    }
103
104    #[test]
105    fn apply_to_omits_unset_options() {
106        let mut params = serde_json::json!([{ "mentions": ["prog"] }, {}]);
107        SubscribeConfig::default().apply_to(&mut params);
108        assert_eq!(params[1].as_object().unwrap().len(), 0);
109    }
110}