buntrail-engine 0.1.0

Authoritative document and workspace orchestration.
Documentation
use super::*;
use crate::document::PendingSave;
use buntrail_core::contracts::*;

fn completion(pending: PendingSave, path: &str, response: Result<Value>) -> Result<String> {
    let value = response?;
    if let Some(receipt) = value.get("receipt") {
        let receipt: OperationReceipt = serde_json::from_value(receipt.clone()).map_err(|_| {
            fail(
                "save_uncertain",
                "Invalid save receipt; inspect the operation",
            )
        })?;
        if receipt.id != pending.operation {
            return Err(fail("save_uncertain", "Save receipt operation changed"));
        }
        match receipt.state {
            OperationState::Confirmed(MutationResult::Saved(saved))
                if saved.saved_revision == pending.revision =>
            {
                use std::os::unix::ffi::OsStrExt;
                let token = string(&value, "version")
                    .map_err(|_| fail("save_uncertain", "Saved binding is missing"))?;
                let binding =
                    Services::file_binding(token).map_err(|e| fail("save_uncertain", e))?;
                let expected =
                    std::path::absolute(path).map_err(|e| fail("save_uncertain", e.to_string()))?;
                let expected = TargetPath::posix(expected.as_os_str().as_bytes().to_vec())
                    .map_err(|e| fail("save_uncertain", e.message))?;
                if binding.version != saved.version || binding.file.path != expected {
                    return Err(fail(
                        "save_uncertain",
                        "Saved binding does not match the submitted file",
                    ));
                }
            }
            OperationState::Failed(failure) if failure.effect == FailureEffect::ProvenNoEffect => {
                return Err(fail("save_failed", failure.error.message));
            }
            _ => {
                return Err(fail(
                    "save_uncertain",
                    "Save outcome is uncertain; retain edits and inspect the save",
                ));
            }
        }
    }
    string(&value, "version").map(str::to_owned).map_err(|_| {
        fail(
            "save_uncertain",
            "Saved file version is missing; inspect the save",
        )
    })
}
impl Engine {
    pub(super) fn save(&self, id: u64) -> Result<Value> {
        self.save_with(id, |write, remote| match remote {
            Some(remote) => self
                .runtime
                .call(&json!({"command":"remote.call","id":remote,"request":write})),
            None => self
                .services
                .save_document(&write)
                .map_err(|e| fail("save_failed", e)),
        })
    }

    pub(super) fn save_with(
        &self,
        id: u64,
        writer: impl FnOnce(Value, Option<String>) -> Result<Value>,
    ) -> Result<Value> {
        let operation = OperationID(
            buntrail_services::connections::local::new_id()
                .map_err(|e| fail("save_failed", e.message))?,
        );
        let (write, remote, journal, prepared, pending, path) = {
            let mut documents = self.documents()?;
            let document = documents
                .open
                .get_mut(&id)
                .ok_or_else(|| fail("not_found", "Document is no longer open"))?;
            if document.saving || document.closing {
                return Err(fail("save_pending", "Wait for pending save or close"));
            }
            if document.pending_save.is_some() {
                return Err(fail(
                    "save_uncertain",
                    "Inspect the uncertain save before retrying",
                ));
            }
            let path = document
                .path
                .clone()
                .ok_or_else(|| fail("unbound_document", "Choose a file before saving"))?;
            let version = document
                .version
                .clone()
                .ok_or_else(|| fail("invalid_request", "Observed file version is missing"))?;
            if document.buffer.has_composition() {
                return Err(fail("composition", "Resolve composition before saving"));
            }
            document.buffer.end_typing();
            let snapshot = document
                .buffer
                .confirmed_snapshot()
                .map_err(crate::document::core_error)?;
            let pending = PendingSave {
                operation,
                revision: BufferRevision(snapshot.revision),
                checkpoint: document.buffer.checkpoint(),
                uncertain: false,
            };
            document.saving = true;
            document.pending_save = Some(pending);
            let prepared = match document.recovery_snapshot(false) {
                Ok(v) => v,
                Err(e) => {
                    document.saving = false;
                    document.pending_save = None;
                    return Err(e);
                }
            };
            let write = json!({"command":"fs.write","path":path,"version":version,"text":snapshot.text.to_string(),"operation":operation,"revision":snapshot.revision});
            (
                write,
                document.remote.clone(),
                document.journal.clone(),
                prepared,
                pending,
                path,
            )
        };
        if let Some(prepared) = prepared
            && let Err(message) = self.services.write_recovery(&journal, prepared)
        {
            if let Some(document) = self.documents()?.open.get_mut(&id) {
                document.saving = false;
                document.pending_save = None;
            }
            return Err(fail("recovery_failed", message));
        }
        let result = completion(pending, &path, writer(write, remote));
        self.finish_save(id, pending, result)
    }
    pub(super) fn inspect_save(&self, id: u64) -> Result<Value> {
        let (pending, version, path, remote) = {
            let mut documents = self.documents()?;
            let document = documents
                .open
                .get_mut(&id)
                .ok_or_else(|| fail("not_found", "Document is no longer open"))?;
            if document.saving || document.closing {
                return Err(fail("save_pending", "Wait for the active operation"));
            }
            let pending = document
                .pending_save
                .ok_or_else(|| fail("invalid_request", "There is no uncertain save"))?;
            let version = document
                .version
                .clone()
                .ok_or_else(|| fail("wrong_file", "File binding is missing"))?;
            document.saving = true;
            let path = document
                .path
                .clone()
                .ok_or_else(|| fail("wrong_file", "File path is missing"))?;
            (pending, version, path, document.remote.clone())
        };
        let response=match remote {
            Some(remote)=>self.runtime.call(&json!({"command":"remote.call","id":remote,"request":{"command":"fs.inspect","path":path,"version":version,"operation":pending.operation}})).map_err(|e|fail("save_uncertain",e.message)),
            None=>self.services.inspect_save(&version,pending.operation).map_err(|e|fail("save_uncertain",e)),
        };
        let result = completion(pending, &path, response);
        self.finish_save(id, pending, result)
    }
    fn finish_save(&self, id: u64, pending: PendingSave, result: Result<String>) -> Result<Value> {
        let (journal, snapshot) = {
            let mut documents = self.documents()?;
            let document = documents
                .open
                .get_mut(&id)
                .ok_or_else(|| fail("not_found", "Document closed during save"))?;
            match &result {
                Ok(version) => {
                    document.version = Some(version.clone());
                    document.saved_state = pending.checkpoint;
                    document.pending_save = None;
                }
                Err(error) if error.code == "save_uncertain" => {
                    document.pending_save = Some(PendingSave {
                        uncertain: true,
                        ..pending
                    });
                }
                Err(_) => document.pending_save = None,
            }
            let snapshot = match document.recovery_snapshot(false) {
                Ok(v) => v,
                Err(e) => {
                    document.saving = false;
                    return Err(e);
                }
            };
            (document.journal.clone(), snapshot)
        };
        let journal_result = if let Some(snapshot) = snapshot {
            self.services
                .write_recovery(&journal, snapshot)
                .map_err(|e| fail("recovery_failed", e))
        } else {
            Ok(())
        };
        let mut documents = self.documents()?;
        let document = documents
            .open
            .get_mut(&id)
            .ok_or_else(|| fail("not_found", "Document closed during save"))?;
        document.saving = false;
        result?;
        journal_result?;
        Ok(document.state(id))
    }
}