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 "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}