Skip to main content

calimero_context/handlers/
delete_context.rs

1use core::error::Error;
2
3use actix::{ActorResponse, ActorTryFutureExt, Handler, Message, WrapFuture};
4use calimero_context_primitives::messages::{DeleteContextRequest, DeleteContextResponse};
5use calimero_node_primitives::client::NodeClient;
6use calimero_primitives::context::ContextId;
7use calimero_store::key::Key;
8use calimero_store::layer::{ReadLayer, WriteLayer};
9use calimero_store::{key, Store};
10use either::Either;
11use eyre::bail;
12
13use crate::ContextManager;
14
15impl Handler<DeleteContextRequest> for ContextManager {
16    type Result = ActorResponse<Self, <DeleteContextRequest as Message>::Result>;
17
18    fn handle(
19        &mut self,
20        DeleteContextRequest { context_id }: DeleteContextRequest,
21        _ctx: &mut Self::Context,
22    ) -> Self::Result {
23        let context = self.contexts.get(&context_id);
24
25        let mut guard = None;
26
27        if let Some(context) = context {
28            guard = Some(context.lock());
29        } else {
30            match self.context_client.has_context(&context_id) {
31                Ok(true) => {}
32                Ok(false) => {
33                    return ActorResponse::reply(Ok(DeleteContextResponse { deleted: false }))
34                }
35                Err(err) => return ActorResponse::reply(Err(err)),
36            }
37        }
38
39        let datastore = self.datastore.clone();
40        let node_client = self.node_client.clone();
41
42        let task = async move {
43            let _guard = match guard {
44                Some(Either::Left(guard)) => Some(guard),
45                Some(Either::Right(task)) => Some(task.await),
46                None => None,
47            };
48
49            delete_context(datastore, node_client, context_id).await?;
50
51            Ok(DeleteContextResponse { deleted: true })
52        };
53
54        ActorResponse::r#async(task.into_actor(self).map_ok(move |res, act, _ctx| {
55            let _ignored = act.contexts.remove(&context_id);
56
57            res
58        }))
59    }
60}
61
62async fn delete_context(
63    datastore: Store,
64    node_client: NodeClient,
65    context_id: ContextId,
66) -> eyre::Result<()> {
67    node_client.unsubscribe(&context_id).await?;
68
69    let mut handle = datastore.handle();
70
71    let key = key::ContextMeta::new(context_id);
72
73    handle.delete(&key)?;
74    handle.delete(&key::ContextConfig::new(context_id))?;
75
76    // fixme! store.handle() is problematic here for lifetime reasons
77    let mut datastore = handle.into_inner();
78
79    delete_context_scoped::<key::ContextIdentity, 32>(&mut datastore, &context_id, [0; 32], None)?;
80
81    delete_context_scoped::<key::ContextState, 32>(&mut datastore, &context_id, [0; 32], None)?;
82
83    // NOTE: We do NOT delete ContextDagDelta entries!
84    // Deltas are part of the immutable distributed DAG and must be kept for:
85    // 1. Other nodes syncing missing parents
86    // 2. Historical integrity and audit trail
87    // 3. Preventing "DeltaNotFound" errors during distributed sync
88    //
89    // Context "deletion" should be a soft delete (marking as inactive/left)
90    // rather than actually removing DAG history. See issue for details.
91
92    Ok(())
93}
94
95#[expect(clippy::unwrap_in_result, reason = "pre-validated")]
96fn delete_context_scoped<K, const N: usize>(
97    datastore: &mut Store,
98    context_id: &ContextId,
99    offset: [u8; N],
100    end: Option<[u8; N]>,
101) -> eyre::Result<()>
102where
103    K: key::FromKeyParts<Error: Error + Send + Sync>,
104{
105    let expected_length = Key::<K::Components>::len();
106
107    if context_id.len().saturating_add(N) != expected_length {
108        bail!(
109            "key length mismatch, expected: {}, got: {}",
110            Key::<K::Components>::len() - N,
111            N
112        )
113    }
114
115    let mut keys = vec![];
116
117    let mut key = context_id.to_vec();
118
119    let end = end
120        .map(|end| {
121            key.extend_from_slice(&end);
122
123            let end = Key::<K::Components>::try_from_slice(&key).expect("length pre-matched");
124
125            K::try_from_parts(end)
126        })
127        .transpose()?;
128
129    'outer: loop {
130        key.truncate(context_id.len());
131        key.extend_from_slice(&offset);
132
133        let offset = Key::<K::Components>::try_from_slice(&key).expect("length pre-matched");
134
135        let mut iter = datastore.iter()?;
136
137        let first = iter.seek(K::try_from_parts(offset)?).transpose();
138
139        if first.is_none() {
140            break;
141        }
142
143        for k in first.into_iter().chain(iter.keys()) {
144            let k = k?;
145
146            let key = k.as_key();
147
148            if let Some(end) = end {
149                if key == end.as_key() {
150                    break 'outer;
151                }
152            }
153
154            if !key.as_bytes().starts_with(&**context_id) {
155                break 'outer;
156            }
157
158            keys.push(k);
159
160            if keys.len() == 100 {
161                break;
162            }
163        }
164
165        drop(iter);
166
167        #[expect(clippy::iter_with_drain, reason = "reallocation would be a bad idea")]
168        for k in keys.drain(..) {
169            datastore.delete(&k)?;
170        }
171    }
172
173    for k in keys {
174        datastore.delete(&k)?;
175    }
176
177    Ok(())
178}