simulator-api 0.9.0

Wire-protocol types for the Solana simulator backtest WebSocket API
Documentation
//! Cross-cutting subscribe options shared by the client and server.
//!
//! In a subscribe request's positional `[filter, config]` params, the `config`
//! object carries per-kind fields (e.g. `commitment`) plus these options. The
//! client builds [`SubscribeConfig`] and merges it in; the server parses it back
//! out — one typed home for the wire field names. Absent fields default off, so
//! peers without support interoperate.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Compression negotiated for notification frames. `lowercase` so the variant
/// matches the on-wire token (`"zstd"`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Compression {
    Zstd,
}

/// Cross-cutting subscribe options the client sets in the config object.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubscribeConfig {
    /// Last slot the client already processed. On reconnect the server replays
    /// from this slot (inclusive) rather than streaming the full history.
    #[serde(
        rename = "replayFromSlot",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub replay_from_slot: Option<i64>,
    /// Notification-frame compression the client opted into. `None` keeps the
    /// uncompressed `Text` path.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub compression: Option<Compression>,
}

impl SubscribeConfig {
    /// Parse the cross-cutting options out of a subscribe config object. Per-kind
    /// fields are ignored, and a missing or malformed object yields defaults
    /// (all off), so a subscribe never fails on these optional fields.
    pub fn from_value(config: Option<&Value>) -> Self {
        config
            .cloned()
            .and_then(|v| serde_json::from_value(v).ok())
            .unwrap_or_default()
    }

    /// Merge the set options into a subscribe request's config object (the second
    /// positional param), leaving its per-kind fields untouched. A no-op if the
    /// params don't have a config object.
    pub fn apply_to(&self, params: &mut Value) {
        let Some(config) = params.get_mut(1).and_then(Value::as_object_mut) else {
            return;
        };
        if let Ok(Value::Object(fields)) = serde_json::to_value(self) {
            config.extend(fields);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_set_options_and_ignores_per_kind_fields() {
        let cfg = SubscribeConfig::from_value(Some(&serde_json::json!({
            "commitment": "confirmed",
            "replayFromSlot": 42,
            "compression": "zstd",
        })));
        assert_eq!(cfg.replay_from_slot, Some(42));
        assert_eq!(cfg.compression, Some(Compression::Zstd));
    }

    #[test]
    fn absent_or_malformed_options_default_off() {
        assert_eq!(
            SubscribeConfig::from_value(None),
            SubscribeConfig::default()
        );
        let cfg =
            SubscribeConfig::from_value(Some(&serde_json::json!({ "commitment": "confirmed" })));
        assert_eq!(cfg, SubscribeConfig::default());
        // An unrecognized compression token leaves compression off.
        let cfg = SubscribeConfig::from_value(Some(&serde_json::json!({ "compression": "gzip" })));
        assert_eq!(cfg.compression, None);
    }

    #[test]
    fn apply_to_merges_into_config_without_clobbering() {
        let mut params =
            serde_json::json!([{ "mentions": ["prog"] }, { "commitment": "confirmed" }]);
        SubscribeConfig {
            replay_from_slot: Some(7),
            compression: Some(Compression::Zstd),
        }
        .apply_to(&mut params);
        assert_eq!(params[1]["commitment"], "confirmed");
        assert_eq!(params[1]["replayFromSlot"], 7);
        assert_eq!(params[1]["compression"], "zstd");
    }

    #[test]
    fn apply_to_omits_unset_options() {
        let mut params = serde_json::json!([{ "mentions": ["prog"] }, {}]);
        SubscribeConfig::default().apply_to(&mut params);
        assert_eq!(params[1].as_object().unwrap().len(), 0);
    }
}