Skip to main content

distributed/mutation/
handler.rs

1//! Portable and custom projection event handlers over mutation programs.
2
3use serde::Serialize;
4use sha2::{Digest, Sha256};
5
6use crate::projection::{
7    ProjectionEventSelector, ProjectionExpression, ProjectionPartition, ProjectionProgramId,
8    ProjectionValueType,
9};
10
11use super::bind::{MutationEventBinding, MutationInputBinding};
12use super::canonical::canonical_json_bytes;
13use super::program::{MutationProgram, MutationProgramId};
14use super::MutationProgramError;
15
16/// Placement for a projector handler registration.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum MutationHandlerPlacement {
20    /// Asynchronous local projector.
21    EventualLocal,
22    /// Asynchronous remote projector.
23    EventualRemote,
24    /// Same-transaction direct projector.
25    Direct,
26}
27
28/// Ownership and topology metadata for one projection arm.
29#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
30pub struct MutationHandlerRegistration {
31    /// Stable projector owner name.
32    owner: String,
33    /// Ownership / rebuild epoch.
34    epoch: String,
35    /// Placement class.
36    placement: MutationHandlerPlacement,
37    /// Logical partition expression encoded as projection partition.
38    #[serde(skip)]
39    partition: ProjectionPartition,
40    /// Event-to-mutation binding.
41    #[serde(skip)]
42    binding: MutationEventBinding,
43    /// Optional human-readable handler name.
44    name: String,
45    /// Independently evolving handler version.
46    version: u64,
47}
48
49impl MutationHandlerRegistration {
50    /// Construct a projection arm registration.
51    ///
52    /// # Errors
53    ///
54    /// Rejects empty names.
55    pub fn try_new(
56        name: impl Into<String>,
57        version: u64,
58        owner: impl Into<String>,
59        epoch: impl Into<String>,
60        placement: MutationHandlerPlacement,
61        partition: ProjectionPartition,
62        binding: MutationEventBinding,
63    ) -> Result<Self, MutationProgramError> {
64        let name = super::expression::non_empty(name.into(), "handler name")?;
65        let owner = super::expression::non_empty(owner.into(), "handler owner")?;
66        let epoch = super::expression::non_empty(epoch.into(), "handler epoch")?;
67        if version == 0 {
68            return Err(MutationProgramError::ZeroVersion("handler version"));
69        }
70        Ok(Self {
71            owner,
72            epoch,
73            placement,
74            partition,
75            binding,
76            name,
77            version,
78        })
79    }
80
81    /// Return the handler name.
82    pub fn name(&self) -> &str {
83        &self.name
84    }
85
86    /// Return the handler version.
87    pub fn version(&self) -> u64 {
88        self.version
89    }
90
91    /// Return the owner name.
92    pub fn owner(&self) -> &str {
93        &self.owner
94    }
95
96    /// Return the ownership epoch.
97    pub fn epoch(&self) -> &str {
98        &self.epoch
99    }
100
101    /// Return placement.
102    pub fn placement(&self) -> MutationHandlerPlacement {
103        self.placement
104    }
105
106    /// Return partition.
107    pub fn partition(&self) -> &ProjectionPartition {
108        &self.partition
109    }
110
111    /// Return the event-to-mutation binding.
112    pub fn binding(&self) -> &MutationEventBinding {
113        &self.binding
114    }
115
116    /// Return the uniqueness key `(owner, event contract, epoch)`.
117    pub fn uniqueness_key(&self) -> MutationHandlerUniquenessKey {
118        MutationHandlerUniquenessKey {
119            owner: self.owner.clone(),
120            event_name: self.binding.selector().event_name().to_owned(),
121            event_version: self.binding.selector().event_version(),
122            body_fingerprint: self.binding.selector().body_fingerprint().to_owned(),
123            epoch: self.epoch.clone(),
124        }
125    }
126
127    /// Derive target models from the bound mutation program.
128    pub fn target_models(&self) -> Vec<String> {
129        let mut models = self
130            .binding
131            .program()
132            .operations()
133            .iter()
134            .map(|operation| operation.target().model().to_owned())
135            .collect::<Vec<_>>();
136        models.sort();
137        models.dedup();
138        models
139    }
140
141    /// Stable digest of owner, event, epoch, placement, mutation program id.
142    ///
143    /// # Errors
144    ///
145    /// Propagates canonical encoding failures.
146    pub fn digest(&self) -> Result<[u8; 32], MutationProgramError> {
147        #[derive(Serialize)]
148        struct DigestBody<'a> {
149            name: &'a str,
150            version: u64,
151            owner: &'a str,
152            epoch: &'a str,
153            placement: MutationHandlerPlacement,
154            mutation_program_id: String,
155            selector_event: &'a str,
156            selector_version: u64,
157            selector_fingerprint: &'a str,
158        }
159        let program_id = self.binding.program().id()?;
160        let body = DigestBody {
161            name: &self.name,
162            version: self.version,
163            owner: &self.owner,
164            epoch: &self.epoch,
165            placement: self.placement,
166            mutation_program_id: program_id.to_string(),
167            selector_event: self.binding.selector().event_name(),
168            selector_version: self.binding.selector().event_version(),
169            selector_fingerprint: self.binding.selector().body_fingerprint(),
170        };
171        let bytes = canonical_json_bytes(&body)?;
172        let mut digest = Sha256::new();
173        digest.update(b"distributed.mutation-handler/v1\0");
174        digest.update((bytes.len() as u64).to_be_bytes());
175        digest.update(&bytes);
176        Ok(digest.finalize().into())
177    }
178
179    /// Materialize the internal projection program for the existing runtime.
180    ///
181    /// # Errors
182    ///
183    /// Propagates rewrite failures.
184    pub fn to_projection_program(
185        &self,
186    ) -> Result<crate::projection::ProjectionProgram, MutationProgramError> {
187        self.binding.to_projection_program(
188            self.name.clone(),
189            self.version,
190            self.partition.clone(),
191            format!("{}-arm", self.name),
192        )
193    }
194}
195
196/// Uniqueness key for projection arm bindings.
197#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
198pub struct MutationHandlerUniquenessKey {
199    /// Projector owner.
200    pub owner: String,
201    /// Semantic event name.
202    pub event_name: String,
203    /// Semantic event version.
204    pub event_version: u64,
205    /// Body fingerprint.
206    pub body_fingerprint: String,
207    /// Ownership epoch.
208    pub epoch: String,
209}
210
211/// Deployment catalog of portable mutation handlers.
212#[derive(Clone, Debug, Default)]
213pub struct MutationHandlerCatalog {
214    registrations: Vec<MutationHandlerRegistration>,
215}
216
217impl MutationHandlerCatalog {
218    /// Construct an empty catalog.
219    pub fn new() -> Self {
220        Self {
221            registrations: Vec::new(),
222        }
223    }
224
225    /// Register a projection arm, rejecting uniqueness and dual-writer conflicts.
226    ///
227    /// # Errors
228    ///
229    /// Rejects duplicate `(owner, event, epoch)` bindings and dual writers for
230    /// the same model/partition/epoch with incompatible placement.
231    pub fn register(
232        &mut self,
233        registration: MutationHandlerRegistration,
234    ) -> Result<(), MutationProgramError> {
235        let key = registration.uniqueness_key();
236        if self
237            .registrations
238            .iter()
239            .any(|existing| existing.uniqueness_key() == key)
240        {
241            return Err(MutationProgramError::InvalidOperation {
242                operation: registration.name().to_owned(),
243                reason: format!(
244                    "duplicate binding for owner `{}` event `{}` v{} epoch `{}`",
245                    key.owner, key.event_name, key.event_version, key.epoch
246                ),
247            });
248        }
249        // Dual-writer check: same model + epoch + overlapping placement classes.
250        for model in registration.target_models() {
251            for existing in &self.registrations {
252                if existing.epoch() != registration.epoch() {
253                    continue;
254                }
255                if !existing.target_models().iter().any(|item| item == &model) {
256                    continue;
257                }
258                let direct_overlap = matches!(
259                    (existing.placement(), registration.placement()),
260                    (
261                        MutationHandlerPlacement::Direct,
262                        MutationHandlerPlacement::Direct
263                    ) | (
264                        MutationHandlerPlacement::Direct,
265                        MutationHandlerPlacement::EventualLocal
266                            | MutationHandlerPlacement::EventualRemote
267                    ) | (
268                        MutationHandlerPlacement::EventualLocal
269                            | MutationHandlerPlacement::EventualRemote,
270                        MutationHandlerPlacement::Direct
271                    )
272                );
273                // Same model+epoch with any two writers is rejected in v1 when
274                // either is direct or both are eventual for the same partition unit.
275                if direct_overlap
276                    || (existing.owner() != registration.owner()
277                        && existing.placement() == registration.placement())
278                {
279                    return Err(MutationProgramError::InvalidOperation {
280                        operation: registration.name().to_owned(),
281                        reason: format!(
282                            "dual writer for model `{model}` epoch `{}` between `{}` and `{}`",
283                            registration.epoch(),
284                            existing.name(),
285                            registration.name()
286                        ),
287                    });
288                }
289            }
290        }
291        self.registrations.push(registration);
292        Ok(())
293    }
294
295    /// Return all registrations.
296    pub fn registrations(&self) -> &[MutationHandlerRegistration] {
297        &self.registrations
298    }
299
300    /// Find registrations for a given event selector.
301    pub fn for_selector(
302        &self,
303        selector: &ProjectionEventSelector,
304    ) -> Vec<&MutationHandlerRegistration> {
305        self.registrations
306            .iter()
307            .filter(|registration| registration.binding().selector() == selector)
308            .collect()
309    }
310}
311
312/// Descriptor for a custom (non-portable) async handler that emits mutations.
313#[derive(Clone, Debug)]
314pub struct CustomMutationHandler {
315    /// Handler name.
316    pub name: String,
317    /// Owner name.
318    pub owner: String,
319    /// Epoch.
320    pub epoch: String,
321    /// Placement.
322    pub placement: MutationHandlerPlacement,
323    /// Event selector.
324    pub selector: ProjectionEventSelector,
325    /// Mutations this custom handler is allowed to emit.
326    pub allowed_programs: Vec<MutationProgramId>,
327}
328
329impl CustomMutationHandler {
330    /// Construct a custom handler descriptor.
331    pub fn new(
332        name: impl Into<String>,
333        owner: impl Into<String>,
334        epoch: impl Into<String>,
335        placement: MutationHandlerPlacement,
336        selector: ProjectionEventSelector,
337        allowed_programs: Vec<MutationProgramId>,
338    ) -> Self {
339        Self {
340            name: name.into(),
341            owner: owner.into(),
342            epoch: epoch.into(),
343            placement,
344            selector,
345            allowed_programs,
346        }
347    }
348
349    /// Custom handlers are never portable to the browser.
350    pub fn is_portable(&self) -> bool {
351        false
352    }
353}
354
355/// Compose a portable binding from event field paths into mutation inputs.
356pub fn portable_binding(
357    selector: ProjectionEventSelector,
358    program: MutationProgram,
359    field_pairs: &[(&[&str], &[&str], ProjectionValueType)],
360) -> Result<MutationEventBinding, MutationProgramError> {
361    let inputs = field_pairs
362        .iter()
363        .map(|(input, body, value_type)| {
364            super::bind::body_field_binding(
365                input.iter().copied(),
366                body.iter().copied(),
367                value_type.clone(),
368            )
369        })
370        .collect::<Result<Vec<_>, _>>()?;
371    MutationEventBinding::try_new(selector, inputs, program)
372}
373
374/// Helper to build an input binding list from explicit expressions.
375pub fn bindings_from_expressions(
376    pairs: Vec<(Vec<String>, ProjectionExpression)>,
377) -> Result<Vec<MutationInputBinding>, MutationProgramError> {
378    pairs
379        .into_iter()
380        .map(|(path, expression)| MutationInputBinding::try_new(path, expression))
381        .collect()
382}
383
384// Keep ProjectionProgramId import used for documentation symmetry.
385#[allow(dead_code)]
386fn _projection_program_id_type(_: ProjectionProgramId) {}