use crate::error::GraphError;
use crate::graph::change::ChangeSet;
pub type HookError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HookRefusal {
message: String,
help: String,
}
impl HookRefusal {
pub fn new(message: impl Into<String>, help: impl Into<String>) -> Self {
HookRefusal {
message: message.into(),
help: help.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
pub fn help(&self) -> &str {
&self.help
}
}
impl std::fmt::Display for HookRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for HookRefusal {}
pub trait CommitHook: Send + Sync {
fn name(&self) -> &str {
std::any::type_name::<Self>()
}
fn before_commit(&mut self, changes: &ChangeSet) -> Result<(), HookError> {
let _ = changes;
Ok(())
}
fn after_commit(&mut self, changes: &ChangeSet) {
let _ = changes;
}
fn after_rollback(&mut self, reason: &RollbackReason) {
let _ = reason;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RollbackReason {
#[non_exhaustive]
Vetoed {
hook: String,
message: String,
},
#[non_exhaustive]
Failed {
message: String,
},
}
impl RollbackReason {
pub fn failed(error: &dyn std::fmt::Display) -> Self {
RollbackReason::Failed {
message: error.to_string(),
}
}
pub fn from_error(error: &GraphError) -> Self {
match error {
GraphError::CommitRejected { hook, source } => RollbackReason::Vetoed {
hook: hook.clone(),
message: source.to_string(),
},
other => RollbackReason::failed(other),
}
}
pub fn message(&self) -> &str {
match self {
RollbackReason::Vetoed { message, .. } | RollbackReason::Failed { message } => message,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Mark {
name: String,
commit_seq: u64,
time: i64,
}
impl Mark {
pub(crate) fn new(name: impl Into<String>, commit_seq: u64, time: i64) -> Self {
Self {
name: name.into(),
commit_seq,
time,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn commit_seq(&self) -> u64 {
self.commit_seq
}
pub fn time(&self) -> i64 {
self.time
}
}
#[cfg_attr(not(feature = "persist"), allow(dead_code))]
pub(crate) trait JournalHook: CommitHook {
fn record_mark(&mut self, mark: &Mark) -> Result<(), HookError>;
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
fn record_checkpoint(&mut self, commit_seq: u64, time: i64) -> Result<(), HookError>;
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
fn sync_now(&mut self) -> Result<(), HookError>;
}
pub struct JournalSlot(pub(crate) Box<dyn JournalHook>);
impl JournalSlot {
#[cfg_attr(not(feature = "persist"), allow(dead_code))]
pub(crate) fn new(journal: Box<dyn JournalHook>) -> Self {
Self(journal)
}
pub fn name(&self) -> &str {
self.0.name()
}
}
impl std::fmt::Debug for JournalSlot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("JournalSlot").field(&self.name()).finish()
}
}
#[derive(Default)]
pub struct CommitHooks {
hooks: Vec<Box<dyn CommitHook>>,
journal: Option<Box<dyn JournalHook>>,
}
impl std::fmt::Debug for CommitHooks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.hooks.iter().map(|hook| hook.name()))
.entries(self.journal.iter().map(|hook| hook.name()))
.finish()
}
}
impl CommitHooks {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn is_active(&self) -> bool {
!self.hooks.is_empty() || self.journal.is_some()
}
pub fn register(&mut self, hook: Box<dyn CommitHook>) {
self.hooks.push(hook);
}
pub fn set_journal(&mut self, journal: JournalSlot) -> Result<(), GraphError> {
if self.journal.is_some() {
return Err(GraphError::Unsupported {
operation: "a second journal on one graph",
});
}
self.journal = Some(journal.0);
Ok(())
}
pub fn has_journal(&self) -> bool {
self.journal.is_some()
}
pub fn before_commit(&mut self, changes: &ChangeSet) -> Result<(), GraphError> {
for hook in &mut self.hooks {
if let Err(source) = hook.before_commit(changes) {
return Err(GraphError::CommitRejected {
hook: hook.name().to_string(),
source,
});
}
}
if let Some(journal) = &mut self.journal
&& let Err(source) = journal.before_commit(changes)
{
return Err(GraphError::CommitRejected {
hook: journal.name().to_string(),
source,
});
}
Ok(())
}
pub fn after_commit(&mut self, changes: &ChangeSet) {
for hook in &mut self.hooks {
hook.after_commit(changes);
}
if let Some(journal) = &mut self.journal {
journal.after_commit(changes);
}
}
pub fn after_rollback(&mut self, reason: &RollbackReason) {
for hook in &mut self.hooks {
hook.after_rollback(reason);
}
if let Some(journal) = &mut self.journal {
journal.after_rollback(reason);
}
}
pub fn mark(
&mut self,
name: &str,
commit_seq: u64,
last_committed_at: i64,
) -> Result<Mark, GraphError> {
if name.is_empty() || name.len() > 255 {
return Err(GraphError::MarkUnavailable {
reason: "a mark name must have 1 to 255 bytes",
});
}
let Some(journal) = self.journal.as_deref_mut() else {
return Err(GraphError::MarkUnavailable {
reason: "no journal is registered on this graph",
});
};
let time = crate::graph::undo::now_micros().max(last_committed_at);
let mark = Mark::new(name, commit_seq, time);
journal
.record_mark(&mark)
.map_err(|source| GraphError::MarkRejected {
hook: journal.name().to_string(),
source,
})?;
Ok(mark)
}
}
impl crate::graph::TraversalGraph {
pub(crate) fn journal_mark(&mut self, name: &str) -> Result<Mark, GraphError> {
if self.undo_depth() > 0 {
return Err(GraphError::MarkUnavailable {
reason: "a mark cannot be set while a unit (a traversal or a transaction) is open",
});
}
let (commit_seq, last_committed_at) = (self.last_commit_seq(), self.last_committed_at());
self.undo.hooks.mark(name, commit_seq, last_committed_at)
}
}