backend/
automerge_json.rs

1//! Utilities for converting between JSON values and Automerge documents.
2
3use crate::app::AppState;
4use crate::document::{RefContent, autosave};
5use automerge::hydrate;
6use automerge::transaction::Transactable;
7use futures_util::stream::StreamExt;
8use samod::DocHandle;
9use serde_json::Value;
10use uuid::Uuid;
11
12/// Insert a JSON value into a map property
13fn insert_value_into_map<'a>(
14    tx: &mut automerge::transaction::Transaction<'a>,
15    parent: &automerge::ObjId,
16    key: &str,
17    value: &Value,
18) -> Result<(), automerge::AutomergeError> {
19    match value {
20        Value::String(s) => {
21            // Use ObjType::Text instead of scalar string to avoid ImmutableString in JavaScript
22            let text_id = tx.put_object(parent, key, automerge::ObjType::Text)?;
23            tx.splice_text(&text_id, 0, 0, s.as_str())?;
24        }
25        Value::Number(n) => {
26            if let Some(i) = n.as_i64() {
27                tx.put(parent, key, i)?;
28            } else if let Some(f) = n.as_f64() {
29                tx.put(parent, key, f)?;
30            }
31        }
32        Value::Bool(b) => {
33            tx.put(parent, key, *b)?;
34        }
35        Value::Null => {
36            tx.put(parent, key, ())?;
37        }
38        Value::Object(map) => {
39            let obj_id = tx.put_object(parent, key, automerge::ObjType::Map)?;
40            for (nested_key, nested_val) in map {
41                insert_value_into_map(tx, &obj_id, nested_key.as_str(), nested_val)?;
42            }
43        }
44        Value::Array(arr) => {
45            let list_id = tx.put_object(parent, key, automerge::ObjType::List)?;
46            for (i, item) in arr.iter().enumerate() {
47                insert_value_into_list(tx, &list_id, i, item)?;
48            }
49        }
50    }
51    Ok(())
52}
53
54/// Insert a JSON value into a list at index
55fn insert_value_into_list<'a>(
56    tx: &mut automerge::transaction::Transaction<'a>,
57    parent: &automerge::ObjId,
58    index: usize,
59    value: &Value,
60) -> Result<(), automerge::AutomergeError> {
61    match value {
62        Value::String(s) => {
63            // Use ObjType::Text instead of scalar string to avoid ImmutableString in JavaScript
64            let text_id = tx.insert_object(parent, index, automerge::ObjType::Text)?;
65            tx.splice_text(&text_id, 0, 0, s.as_str())?;
66        }
67        Value::Number(n) => {
68            if let Some(i) = n.as_i64() {
69                tx.insert(parent, index, i)?;
70            } else if let Some(f) = n.as_f64() {
71                tx.insert(parent, index, f)?;
72            }
73        }
74        Value::Bool(b) => {
75            tx.insert(parent, index, *b)?;
76        }
77        Value::Null => {
78            tx.insert(parent, index, ())?;
79        }
80        Value::Object(map) => {
81            let obj_id = tx.insert_object(parent, index, automerge::ObjType::Map)?;
82            for (nested_key, nested_val) in map {
83                insert_value_into_map(tx, &obj_id, nested_key.as_str(), nested_val)?;
84            }
85        }
86        Value::Array(arr) => {
87            let list_id = tx.insert_object(parent, index, automerge::ObjType::List)?;
88            for (i, item) in arr.iter().enumerate() {
89                insert_value_into_list(tx, &list_id, i, item)?;
90            }
91        }
92    }
93    Ok(())
94}
95
96/// Populate an automerge document from a JSON value.
97pub(crate) fn populate_automerge_from_json<'a>(
98    tx: &mut automerge::transaction::Transaction<'a>,
99    obj_id: automerge::ObjId,
100    value: &Value,
101) -> Result<(), automerge::AutomergeError> {
102    let Value::Object(map) = value else {
103        let value_type = match value {
104            Value::Null => "Null",
105            Value::Bool(_) => "Bool",
106            Value::Number(_) => "Number",
107            Value::String(_) => "String",
108            Value::Array(_) => "Array",
109            Value::Object(_) => unreachable!(),
110        };
111
112        return Err(automerge::AutomergeError::InvalidValueType {
113            expected: "Object".to_string(),
114            unexpected: format!("{} as document root", value_type),
115        });
116    };
117
118    for (key, val) in map {
119        insert_value_into_map(tx, &obj_id, key.as_str(), val)?;
120    }
121
122    Ok(())
123}
124
125/// Convert automerge hydrate::Value to serde_json::Value
126pub(crate) fn hydrate_to_json(value: &hydrate::Value) -> Value {
127    match value {
128        hydrate::Value::Scalar(s) => scalar_to_json(s),
129        hydrate::Value::Map(m) => {
130            let mut map = serde_json::Map::new();
131            for (key, map_value) in m.iter() {
132                map.insert(key.to_string(), hydrate_to_json(&map_value.value));
133            }
134            Value::Object(map)
135        }
136        hydrate::Value::List(l) => {
137            Value::Array(l.iter().map(|list_value| hydrate_to_json(&list_value.value)).collect())
138        }
139        hydrate::Value::Text(t) => Value::String(t.to_string()),
140    }
141}
142
143fn scalar_to_json(s: &automerge::ScalarValue) -> Value {
144    use automerge::ScalarValue;
145    match s {
146        ScalarValue::Bytes(b) => {
147            Value::Array(b.iter().map(|v| Value::Number((*v).into())).collect())
148        }
149        ScalarValue::Str(s) => Value::String(s.to_string()),
150        ScalarValue::Int(i) => Value::Number((*i).into()),
151        ScalarValue::Uint(u) => Value::Number((*u).into()),
152        ScalarValue::F64(f) => {
153            serde_json::Number::from_f64(*f).map(Value::Number).unwrap_or(Value::Null)
154        }
155        ScalarValue::Counter(c) => Value::Number(i64::from(c).into()),
156        ScalarValue::Timestamp(t) => Value::Number((*t).into()),
157        ScalarValue::Boolean(b) => Value::Bool(*b),
158        ScalarValue::Null => Value::Null,
159        ScalarValue::Unknown { type_code, bytes } => Value::Object(serde_json::Map::from_iter([
160            ("type_code".to_string(), Value::Number((*type_code).into())),
161            (
162                "bytes".to_string(),
163                Value::Array(bytes.iter().map(|b| Value::Number((*b).into())).collect()),
164            ),
165        ])),
166    }
167}
168
169/// Spawns a background task that listens for document changes and triggers autosave.
170pub(crate) async fn ensure_autosave_listener(state: AppState, ref_id: Uuid, doc_handle: DocHandle) {
171    let listeners = state.active_listeners.read().await;
172    if listeners.contains(&ref_id) {
173        return;
174    }
175
176    // Explicitly drop the read lock before acquiring write lock
177    drop(listeners);
178
179    let mut listeners = state.active_listeners.write().await;
180    listeners.insert(ref_id);
181
182    tokio::spawn({
183        let state = state.clone();
184        async move {
185            let mut changes = doc_handle.changes();
186
187            while (changes.next().await).is_some() {
188                let cloned_doc = doc_handle.with_document(|doc| doc.clone());
189                let hydrated = cloned_doc.hydrate(None);
190                let content = hydrate_to_json(&hydrated);
191
192                let data = RefContent { ref_id, content };
193                if let Err(e) = autosave(state.clone(), data).await {
194                    tracing::error!("Autosave failed for ref {}: {:?}", ref_id, e);
195                }
196            }
197
198            state.active_listeners.write().await.remove(&ref_id);
199            tracing::error!("Autosave listener stopped for ref {}", ref_id);
200        }
201    });
202}