mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 03:16:53 -06:00
* M9: collaboration: per-user undo, rejections, reconnect
Step 3 of docs/collab-protocol.md, the parts that don't need the transport:
- Per-user undo and redo (figcraft_collab::Client::{undo, redo}): each undoable batch is a step
(values before/after by target, nodes created/deleted). Others' batches ordered after the step
take its targets for good; creates invert to deletes (not when others built inside), deletes
re-create the subtree. An undo or redo waits for every earlier batch and is remade from the
confirmed state when it goes out; derived data follows as a follow-up batch.
- Rejections: refused batches leave the history (with an undo waiting on them) and queue a
Notice; the engine shows it through Session::message (the UI's toast).
- Offline queue and reconnect: Hello.resume {session, client_id, seq}; the server keeps a
bounded log and answers with the missed batches or a snapshot, plus what it did with the
client's batches (processed {last_batch, refused}); queued batches then go out.
Server::state / restore persist the session (client-id counter, log, session id).
- Presence carries editing_text {node, caret, selection}.
- Engine: documents in a live session (LiveDoc, Session::collab_*): commands, gestures and
groups sync as one step each; edit.undo / edit.redo route to the per-user history; remote
edits show under a gesture in progress. Outside a session snapshot undo is unchanged.
- Sim: dropped connections, undo/redo, and a checker that no undo reverts a change by someone
else its author had seen. New tests in undo_tests, reconnect_tests, collab_live_tests,
collab_undo_tests.
- CLAUDE.md / AGENTS.md: collab in the L2 layer list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: collaboration wording in the progress docs after per-user undo and reconnect
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: protocol doc: what the engine's live-session tests cover
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
53 lines
2.2 KiB
Rust
53 lines
2.2 KiB
Rust
//! The engine's side of real-time collaboration (`figcraft_collab`): its normalize pass as the
|
|
//! recompute hook, and `collab.diff`, which shows the change batch an edit syncs as.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::sync::Arc;
|
|
|
|
use figcraft_collab::{Recompute, Replica, diff, message};
|
|
use figcraft_doc::{Document, NodeId};
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::cmd::CommandSpec;
|
|
use crate::params::string;
|
|
use crate::{EngineError, Result, Session, bad, normalize, spec};
|
|
|
|
/// Derived data follows the engine's normalize pass: text boxes, groups, instances following
|
|
/// their mains, variable bindings and auto layout, recomputed over the touched nodes.
|
|
pub struct EngineRecompute;
|
|
|
|
impl Recompute for EngineRecompute {
|
|
fn recompute(&self, doc: &mut Document, before: &Document, touched: &BTreeSet<NodeId>) {
|
|
doc.mark_dirty(touched.iter().copied());
|
|
normalize(doc, before);
|
|
}
|
|
}
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![spec!(
|
|
"collab.diff",
|
|
"Show sync changes",
|
|
"{against? (undo: the last undoable action, the default | saved: since the last save)} → the changes a live session would send, their count and binary size",
|
|
false,
|
|
diff_cmd
|
|
)]
|
|
}
|
|
|
|
fn diff_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let ds = s.ds()?;
|
|
let from: Arc<Document> = match string(p, "against").as_deref().unwrap_or("undo") {
|
|
"undo" if s.live_doc(ds.uid) => {
|
|
return Err(EngineError::Disabled("collab.diff".into(), "in a live session undo is per user: compare against \"saved\"".into()));
|
|
}
|
|
"undo" => {
|
|
ds.history.undo.last().map(|e| e.doc.clone()).ok_or_else(|| EngineError::Disabled("collab.diff".into(), "nothing to undo".into()))?
|
|
}
|
|
"saved" => ds.saved_doc.clone(),
|
|
other => return Err(bad("collab.diff", format!("`against` is \"undo\" or \"saved\", not {other:?}"))),
|
|
};
|
|
let base = Replica::new((*from).clone());
|
|
let d = diff(&base, &ds.doc).map_err(|e| EngineError::Other(e.to_string()))?;
|
|
let bytes = message::encode(&d.changes).map_err(|e| EngineError::Other(e.to_string()))?.len();
|
|
Ok(json!({"changes": d.changes, "count": d.changes.len(), "bytes": bytes}))
|
|
}
|