Skip to main content

dialcache/
codec.rs

1//! Serialized payloads and value codecs.
2
3use std::borrow::Cow;
4use std::sync::Arc;
5
6use futures::future::BoxFuture;
7use serde::de::DeserializeOwned;
8use serde::Serialize;
9
10use crate::error::BoxError;
11
12/// Serializer output: UTF-8 text or opaque binary bytes.
13///
14/// Text payloads are stored with the frame's UTF-8 tag; binary payloads keep
15/// their exact bytes. Compression envelopes wrap either form transparently.
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
17pub struct Payload {
18    /// The serializer output. Read as UTF-8 unless `binary`; ill-formed text
19    /// sequences are replaced when the payload is stored.
20    pub bytes: Vec<u8>,
21    /// `true` for binary output, `false` for UTF-8 text.
22    pub binary: bool,
23}
24
25impl Payload {
26    /// A UTF-8 text payload.
27    pub fn text(text: impl Into<String>) -> Self {
28        Payload {
29            bytes: text.into().into_bytes(),
30            binary: false,
31        }
32    }
33
34    /// An opaque binary payload, stored byte-exact.
35    pub fn binary(bytes: impl Into<Vec<u8>>) -> Self {
36        Payload {
37            bytes: bytes.into(),
38            binary: true,
39        }
40    }
41
42    /// Length in bytes.
43    pub fn len(&self) -> usize {
44        self.bytes.len()
45    }
46
47    /// Whether the payload holds no bytes.
48    pub fn is_empty(&self) -> bool {
49        self.bytes.is_empty()
50    }
51
52    /// The bytes as text, replacing ill-formed UTF-8 in binary payloads.
53    pub fn as_text(&self) -> Cow<'_, str> {
54        String::from_utf8_lossy(&self.bytes)
55    }
56}
57
58/// Encodes cached values to payloads and decodes them back.
59///
60/// Encoding and decoding are asynchronous so a codec may await. For ordinary
61/// synchronous serializers implement [`SyncCodec`] and wrap the codec in
62/// [`FromSync`]. Every decode must return an independent value: the cache may
63/// retain one payload and decode it more than once.
64pub trait Codec<T>: Send + Sync + 'static {
65    /// Serialize `value`. A failure counts as a `serialization_dump` error
66    /// and skips the remote write.
67    fn encode<'a>(&'a self, value: &'a T) -> BoxFuture<'a, Result<Payload, BoxError>>;
68
69    /// Serialize a shared value owned by the cache.
70    ///
71    /// The engine calls this method for remote writes. By default it delegates
72    /// to [`encode`](Self::encode) without copying the value. Override it to
73    /// move the shared handle into a background job without requiring `T: Clone`.
74    /// The codec owns scheduling, admission and the lifetime of any jobs it
75    /// starts; those jobs do not inherit the cache's shadow-capacity token.
76    fn encode_owned(&self, value: Arc<T>) -> BoxFuture<'_, Result<Payload, BoxError>>
77    where
78        T: Send + Sync + 'static,
79    {
80        Box::pin(async move { self.encode(value.as_ref()).await })
81    }
82
83    /// Deserialize `payload` into an independent value. A failure counts as
84    /// a `serialization_load` error and is treated as a miss.
85    fn decode(&self, payload: Payload) -> BoxFuture<'_, Result<T, BoxError>>;
86}
87
88/// A synchronous codec. Wrap it in [`FromSync`] where a [`Codec`] is expected.
89pub trait SyncCodec<T>: Send + Sync + 'static {
90    /// Serialize `value` without awaiting.
91    fn encode(&self, value: &T) -> Result<Payload, BoxError>;
92    /// Deserialize `payload` into an independent value without awaiting.
93    fn decode(&self, payload: Payload) -> Result<T, BoxError>;
94}
95
96/// Adapts a [`SyncCodec`] to [`Codec`].
97#[derive(Debug, Clone, Copy, Default)]
98pub struct FromSync<C>(pub C);
99
100impl<T: Send + 'static, C: SyncCodec<T>> Codec<T> for FromSync<C> {
101    fn encode<'a>(&'a self, value: &'a T) -> BoxFuture<'a, Result<Payload, BoxError>> {
102        let result = self.0.encode(value);
103        Box::pin(std::future::ready(result))
104    }
105
106    fn decode(&self, payload: Payload) -> BoxFuture<'_, Result<T, BoxError>> {
107        let result = self.0.decode(payload);
108        Box::pin(std::future::ready(result))
109    }
110}
111
112/// Payload text that TypeScript writes for an `undefined` value.
113pub const JSON_UNDEFINED_SENTINEL: &str = "__dialcache_json_undefined_v1__";
114
115/// The default codec: `serde_json` text.
116///
117/// Values written by TypeScript's default serializer decode as long as they
118/// are valid JSON for the destination type. The TypeScript `undefined`
119/// sentinel decodes as JSON `null`: an `Option<T>` destination reads it as
120/// `None`, a destination that accepts `null` (such as `serde_json::Value`)
121/// decodes it, and any other destination fails open to the source.
122#[derive(Debug, Clone, Copy, Default)]
123pub struct JsonCodec;
124
125impl JsonCodec {
126    /// Serialize any `Serialize` value as compact JSON text.
127    pub fn encode_value<T: Serialize>(value: &T) -> Result<Payload, BoxError> {
128        Ok(Payload::text(serde_json::to_string(value)?))
129    }
130
131    /// Deserialize JSON text, reading the TypeScript `undefined` sentinel as
132    /// JSON `null`.
133    pub fn decode_value<T: DeserializeOwned>(payload: &Payload) -> Result<T, BoxError> {
134        let text = payload.as_text();
135        if text == JSON_UNDEFINED_SENTINEL {
136            return Ok(T::deserialize(serde_json::Value::Null)?);
137        }
138        Ok(serde_json::from_str(&text)?)
139    }
140}
141
142impl<T: Serialize + DeserializeOwned + Send + Sync + 'static> Codec<T> for JsonCodec {
143    fn encode<'a>(&'a self, value: &'a T) -> BoxFuture<'a, Result<Payload, BoxError>> {
144        Box::pin(std::future::ready(JsonCodec::encode_value(value)))
145    }
146
147    fn decode(&self, payload: Payload) -> BoxFuture<'_, Result<T, BoxError>> {
148        Box::pin(std::future::ready(JsonCodec::decode_value(&payload)))
149    }
150}