pub mod bus;
pub mod decorator;
pub mod keyspace;
pub mod transport;
pub use bus::{InvalidationBus, InvalidationConfig, ListenerHandle};
pub use decorator::InvalidatingBackend;
pub use keyspace::{KeyspaceNotificationConfig, KeyspaceNotificationListener};
pub use transport::{
InMemoryPubSubTransport, PubSubTransport, RedisPubSubTransport, SubscriptionReceiver,
};
use crate::error::{OxCacheError, OxCacheResult};
use serde::{Deserialize, Serialize};
pub const DEFAULT_CHANNEL: &str = "oxcache:invalidate";
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum InvalidationKind {
Key,
Namespace,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InvalidationMessage {
pub instance_id: String,
pub kind: InvalidationKind,
pub target: String,
pub timestamp_ms: u64,
}
impl InvalidationMessage {
pub fn key(instance_id: impl Into<String>, key: impl Into<String>) -> Self {
Self {
instance_id: instance_id.into(),
kind: InvalidationKind::Key,
target: key.into(),
timestamp_ms: now_ms(),
}
}
pub fn namespace(instance_id: impl Into<String>, namespace: impl Into<String>) -> Self {
Self {
instance_id: instance_id.into(),
kind: InvalidationKind::Namespace,
target: namespace.into(),
timestamp_ms: now_ms(),
}
}
pub fn encode(&self) -> OxCacheResult<String> {
serde_json::to_string(self).map_err(|e| OxCacheError::Serialization(e.to_string()))
}
pub fn decode(payload: &str) -> OxCacheResult<Self> {
serde_json::from_str(payload).map_err(|e| OxCacheError::Serialization(e.to_string()))
}
pub fn is_from(&self, instance_id: &str) -> bool {
self.instance_id == instance_id
}
}
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO)
.as_millis() as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_key_roundtrip() {
let msg = InvalidationMessage::key("inst-a", "user:1");
assert_eq!(msg.kind, InvalidationKind::Key);
assert_eq!(msg.target, "user:1");
let encoded = msg.encode().unwrap();
let decoded = InvalidationMessage::decode(&encoded).unwrap();
assert_eq!(msg, decoded);
}
#[test]
fn message_namespace_roundtrip() {
let msg = InvalidationMessage::namespace("inst-b", "users:");
assert_eq!(msg.kind, InvalidationKind::Namespace);
let decoded = InvalidationMessage::decode(&msg.encode().unwrap()).unwrap();
assert_eq!(msg, decoded);
}
#[test]
fn message_wire_format_is_snake_case_json() {
let msg = InvalidationMessage::key("i", "k");
let wire = msg.encode().unwrap();
assert!(wire.contains("\"kind\":\"key\""), "wire: {wire}");
assert!(wire.contains("\"instance_id\":\"i\""), "wire: {wire}");
assert!(wire.contains("\"target\":\"k\""), "wire: {wire}");
}
#[test]
fn message_decode_rejects_garbage() {
assert!(InvalidationMessage::decode("not json").is_err());
}
#[test]
fn message_self_exemption_check() {
let msg = InvalidationMessage::key("inst-a", "k");
assert!(msg.is_from("inst-a"));
assert!(!msg.is_from("inst-b"));
}
}