Skip to main content

loro_common/
error.rs

1use std::{
2    backtrace::{Backtrace, BacktraceStatus},
3    panic::Location,
4};
5
6use serde_columnar::ColumnarError;
7use thiserror::Error;
8
9use crate::{ContainerID, InternalString, PeerID, TreeID, ID};
10
11pub type LoroResult<T> = Result<T, LoroError>;
12
13#[derive(Error, Debug, PartialEq)]
14pub enum LoroError {
15    #[error("Context's client_id({found:?}) does not match Container's client_id({expected:?})")]
16    UnmatchedContext { expected: PeerID, found: PeerID },
17    #[error("Decode error: Version vector error. Please provide correct version.")]
18    DecodeVersionVectorError,
19    #[error("Decode error: ({0})")]
20    DecodeError(Box<str>),
21    #[error(
22        // This should not happen after v1.0.0
23        "Decode error: The data is either corrupted or originates from an older version that is incompatible due to a breaking change."
24    )]
25    DecodeDataCorruptionError,
26    #[error("Decode error: Checksum mismatch. The data is corrupted.")]
27    DecodeChecksumMismatchError,
28    #[error("Decode error: Encoding version \"{0}\" is incompatible. Loro's encoding is backward compatible but not forward compatible. Please upgrade the version of Loro to support this version of the exported data.")]
29    IncompatibleFutureEncodingError(usize),
30    #[error("Js error ({0})")]
31    JsError(Box<str>),
32    #[error("Cannot get lock or the lock is poisoned")]
33    LockError,
34    #[error("Each AppState can only have one transaction at a time")]
35    DuplicatedTransactionError,
36    #[error("Cannot find ({0})")]
37    NotFoundError(Box<str>),
38    #[error("Transaction error ({0})")]
39    TransactionError(Box<str>),
40    #[error("Index out of bound. The given pos is {pos}, but the length is {len}. {info}")]
41    OutOfBound {
42        pos: usize,
43        len: usize,
44        info: Box<str>,
45    },
46    #[error("Every op id should be unique. ID {id} has been used. You should use a new PeerID to edit the content. ")]
47    UsedOpID { id: ID },
48    #[error("Concurrent ops with the same peer id is not allowed. PeerID: {peer}, LastCounter: {last_counter}, CurrentCounter: {current}")]
49    ConcurrentOpsWithSamePeerID {
50        peer: PeerID,
51        last_counter: i32,
52        current: i32,
53    },
54    #[error("Movable Tree Error: {0}")]
55    TreeError(#[from] LoroTreeError),
56    #[error("Invalid argument ({0})")]
57    ArgErr(Box<str>),
58    #[error("Auto commit has not started. The doc is readonly when detached and detached editing is not enabled.")]
59    AutoCommitNotStarted,
60    #[error("Style configuration missing for \"({0:?})\". Please provide the style configuration using `configTextStyle` on your Loro doc.")]
61    StyleConfigMissing(InternalString),
62    #[error("Unknown Error ({0})")]
63    Unknown(Box<str>),
64    #[error("The given ID ({0}) is not contained by the doc")]
65    FrontiersNotFound(ID),
66    #[error("Cannot import when the doc is in a transaction")]
67    ImportWhenInTxn,
68    #[error("The given method ({method}) is not allowed when the container is detached. You should insert the container to the doc first.")]
69    MisuseDetachedContainer { method: &'static str },
70    #[error("Not implemented: {0}")]
71    NotImplemented(&'static str),
72    #[error("Reattach a container that is already attached")]
73    ReattachAttachedContainer,
74    #[error("Edit is not allowed when the doc is in the detached mode.")]
75    EditWhenDetached,
76    #[error("The given ID ({0}) is not contained by the doc")]
77    UndoInvalidIdSpan(ID),
78    #[error("PeerID cannot be changed. Expected: {expected:?}, Actual: {actual:?}")]
79    UndoWithDifferentPeerId { expected: PeerID, actual: PeerID },
80    #[error("There is already an active undo group, call `group_end` first")]
81    UndoGroupAlreadyStarted,
82    #[error("There is no active undo group, call `group_start` first")]
83    InvalidJsonSchema,
84    #[error("Cannot insert or delete utf-8 in the middle of the codepoint in Unicode")]
85    UTF8InUnicodeCodePoint { pos: usize },
86    #[error("Cannot insert or delete utf-16 in the middle of the codepoint in Unicode")]
87    UTF16InUnicodeCodePoint { pos: usize },
88    #[error("The end index cannot be less than the start index")]
89    EndIndexLessThanStartIndex { start: usize, end: usize },
90    #[error("Invalid root container name! Don't include '/' or '\\0'")]
91    InvalidRootContainerName,
92    #[error("Import Failed: The dependencies of the importing updates are not included in the shallow history of the doc.")]
93    ImportUpdatesThatDependsOnOutdatedVersion,
94    #[error(
95        "You cannot switch a document to a version before the shallow history's start version."
96    )]
97    SwitchToVersionBeforeShallowRoot,
98    #[error(
99        "The container {container} is deleted. You cannot apply the op on a deleted container."
100    )]
101    ContainerDeleted { container: Box<ContainerID> },
102    #[error("You cannot set the `PeerID` with `PeerID::MAX`, which is an internal specific value")]
103    InvalidPeerID,
104    #[error("The containers {containers:?} are not found in the doc")]
105    ContainersNotFound { containers: Box<Vec<ContainerID>> },
106    #[error("Import failed: Deprecated encoding mode")]
107    ImportUnsupportedEncodingMode,
108}
109
110impl LoroError {
111    #[track_caller]
112    pub fn internal(message: impl Into<String>) -> Self {
113        Self::Unknown(format_internal_error_message(
114            message.into(),
115            Location::caller(),
116        ))
117    }
118}
119
120#[derive(Error, Debug, PartialEq)]
121pub enum LoroTreeError {
122    #[error("`Cycle move` occurs when moving tree nodes.")]
123    CyclicMoveError,
124    #[error("The provided parent id is invalid")]
125    InvalidParent,
126    #[error("The parent of tree node is not found {0:?}")]
127    TreeNodeParentNotFound(TreeID),
128    #[error("TreeID {0:?} doesn't exist")]
129    TreeNodeNotExist(TreeID),
130    #[error("The index({index}) should be <= the length of children ({len})")]
131    IndexOutOfBound { len: usize, index: usize },
132    #[error("Fractional index is not enabled, you should enable it first by `LoroTree::set_enable_fractional_index`")]
133    FractionalIndexNotEnabled,
134    #[error("TreeID {0:?} is deleted or does not exist")]
135    TreeNodeDeletedOrNotExist(TreeID),
136}
137
138#[non_exhaustive]
139#[derive(Error, Debug, PartialEq)]
140pub enum LoroEncodeError {
141    #[error("The frontiers are not found in this doc: {0}")]
142    FrontiersNotFound(String),
143    #[error("Shallow snapshot incompatible with old snapshot format. Use new snapshot format or avoid shallow snapshots for storage.")]
144    ShallowSnapshotIncompatibleWithOldFormat,
145    #[error("Cannot export shallow snapshot with unknown container type. Please upgrade the Loro version.")]
146    UnknownContainer,
147    #[error("Export failed: {0}")]
148    InternalError(Box<str>),
149}
150
151impl LoroEncodeError {
152    #[track_caller]
153    pub fn internal(message: impl Into<String>) -> Self {
154        Self::InternalError(format_internal_error_message(
155            message.into(),
156            Location::caller(),
157        ))
158    }
159}
160
161#[cfg(feature = "wasm")]
162pub mod wasm {
163    use wasm_bindgen::JsValue;
164
165    use crate::{LoroEncodeError, LoroError};
166
167    impl From<LoroError> for JsValue {
168        fn from(value: LoroError) -> Self {
169            JsValue::from_str(&value.to_string())
170        }
171    }
172
173    impl From<LoroEncodeError> for JsValue {
174        fn from(value: LoroEncodeError) -> Self {
175            JsValue::from_str(&value.to_string())
176        }
177    }
178
179    impl From<JsValue> for LoroError {
180        fn from(v: JsValue) -> Self {
181            Self::JsError(
182                v.as_string()
183                    .unwrap_or_else(|| "unknown error".to_owned())
184                    .into_boxed_str(),
185            )
186        }
187    }
188}
189
190impl From<ColumnarError> for LoroError {
191    fn from(e: ColumnarError) -> Self {
192        match e {
193            ColumnarError::ColumnarDecodeError(_)
194            | ColumnarError::RleEncodeError(_)
195            | ColumnarError::RleDecodeError(_)
196            | ColumnarError::OverflowError => {
197                LoroError::DecodeError(format!("Failed to decode Columnar: {e}").into_boxed_str())
198            }
199            e => LoroError::Unknown(e.to_string().into_boxed_str()),
200        }
201    }
202}
203
204impl From<LoroEncodeError> for LoroError {
205    fn from(value: LoroEncodeError) -> Self {
206        match value {
207            LoroEncodeError::FrontiersNotFound(frontiers) => {
208                LoroError::NotFoundError(frontiers.into_boxed_str())
209            }
210            LoroEncodeError::ShallowSnapshotIncompatibleWithOldFormat
211            | LoroEncodeError::UnknownContainer => {
212                LoroError::Unknown(value.to_string().into_boxed_str())
213            }
214            LoroEncodeError::InternalError(msg) => LoroError::Unknown(msg),
215        }
216    }
217}
218
219impl From<LoroError> for LoroEncodeError {
220    fn from(value: LoroError) -> Self {
221        match value {
222            LoroError::FrontiersNotFound(id) => {
223                LoroEncodeError::FrontiersNotFound(format!("{id:?}"))
224            }
225            LoroError::NotFoundError(msg) => LoroEncodeError::FrontiersNotFound(msg.into()),
226            LoroError::Unknown(msg) => LoroEncodeError::InternalError(msg),
227            other => LoroEncodeError::InternalError(other.to_string().into_boxed_str()),
228        }
229    }
230}
231
232fn format_internal_error_message(
233    message: String,
234    location: &'static Location<'static>,
235) -> Box<str> {
236    let mut formatted = format!(
237        "{message} at {}:{}:{}",
238        location.file(),
239        location.line(),
240        location.column()
241    );
242    let backtrace = Backtrace::capture();
243    if backtrace.status() == BacktraceStatus::Captured {
244        formatted.push_str("\nBacktrace:\n");
245        formatted.push_str(&backtrace.to_string());
246    }
247
248    formatted.into_boxed_str()
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn internal_loro_error_contains_caller_location() {
257        let err = LoroError::internal("boom");
258        let LoroError::Unknown(msg) = err else {
259            panic!("expected unknown error");
260        };
261        assert!(msg.contains("boom"));
262        assert!(msg.contains("crates/loro-common/src/error.rs"));
263    }
264
265    #[test]
266    fn internal_encode_error_contains_caller_location() {
267        let err = LoroEncodeError::internal("boom");
268        let LoroEncodeError::InternalError(msg) = err else {
269            panic!("expected internal encode error");
270        };
271        assert!(msg.contains("boom"));
272        assert!(msg.contains("crates/loro-common/src/error.rs"));
273    }
274}