lash_core/triggers/
mutation.rs1use super::*;
2
3pub fn evaluate_trigger_mutation(
7 current: Option<TriggerSubscriptionRecord>,
8 command: TriggerCommand,
9 now: u64,
10) -> Result<TriggerEffectResult, PluginError> {
11 if !command.is_mutation() {
12 return Err(PluginError::Session(
13 "trigger mutation evaluator received a list command".to_string(),
14 ));
15 }
16 let mut state = InMemoryTriggerEventState::default();
17 if let Some(record) = current {
18 state
19 .subscriptions
20 .insert(record.subscription_id.clone(), record);
21 }
22 Ok(apply_in_memory_trigger_command(&mut state, command, now))
23}
24
25pub(super) fn mutate_enabled(
26 state: &mut InMemoryTriggerEventState,
27 owner_scope: TriggerOwnerScope,
28 actor: crate::ProcessOriginator,
29 subscription_key: String,
30 expected_revision: u64,
31 enabled: bool,
32 now: u64,
33) -> TriggerEffectResult {
34 let subscription_id = deterministic_subscription_id(&owner_scope, &subscription_key)
35 .map_err(TriggerOperationError::from)?;
36 let Some(existing) = state.subscriptions.get_mut(&subscription_id) else {
37 return Err(subscription_conflict(
38 &subscription_key,
39 None,
40 None,
41 "subscription does not exist",
42 ));
43 };
44 ensure_live_revision(existing, expected_revision, None)?;
45 if existing.enabled != enabled {
46 existing.enabled = enabled;
47 existing.registrant = actor;
48 existing.revision = existing.revision.saturating_add(1);
49 existing.updated_at_ms = now;
50 }
51 let disposition = if enabled {
52 TriggerMutationDisposition::Enabled
53 } else {
54 TriggerMutationDisposition::Disabled
55 };
56 Ok(TriggerCommandOutcome::Mutation {
57 receipt: Box::new(TriggerMutationReceipt::from_record(
58 existing.clone(),
59 disposition,
60 )),
61 })
62}
63
64pub(super) fn ensure_live_revision(
65 existing: &TriggerSubscriptionRecord,
66 expected_revision: u64,
67 requested_hash: Option<String>,
68) -> Result<(), TriggerOperationError> {
69 if existing.tombstoned || existing.revision != expected_revision {
70 return Err(subscription_conflict(
71 &existing.subscription_key,
72 Some(existing),
73 requested_hash,
74 if existing.tombstoned {
75 "subscription is tombstoned"
76 } else {
77 "expected revision does not match"
78 },
79 ));
80 }
81 Ok(())
82}
83
84pub(super) fn subscription_conflict(
85 subscription_key: &str,
86 existing: Option<&TriggerSubscriptionRecord>,
87 requested_definition_hash: Option<String>,
88 reason: &str,
89) -> TriggerOperationError {
90 TriggerOperationError::Conflict {
91 subscription_key: subscription_key.to_string(),
92 existing_revision: existing.map(|record| record.revision),
93 existing_definition_hash: existing.map(|record| record.definition_hash.clone()),
94 requested_definition_hash,
95 reason: reason.to_string(),
96 }
97}
98
99#[allow(clippy::too_many_arguments)]
100pub(super) fn subscription_record_from_draft(
101 owner_scope: TriggerOwnerScope,
102 actor: crate::ProcessOriginator,
103 draft: TriggerSubscriptionDraft,
104 subscription_id: String,
105 incarnation: String,
106 revision: u64,
107 definition_hash: String,
108 enabled: bool,
109 created_at_ms: u64,
110 updated_at_ms: u64,
111) -> TriggerSubscriptionRecord {
112 TriggerSubscriptionRecord {
113 subscription_id,
114 owner_scope,
115 subscription_key: draft.subscription_key,
116 incarnation,
117 revision,
118 definition_hash,
119 registrant: actor,
120 env_ref: draft.env_ref,
121 wake_target: draft.wake_target,
122 name: draft.name,
123 source_type: draft.source_type,
124 source_key: draft.source_key,
125 source: draft.source,
126 payload_schema: draft.payload_schema,
127 target: draft.target,
128 target_identity: draft.target_identity,
129 event_types: draft.event_types,
130 input_template: draft.input_template,
131 target_label: draft.target_label,
132 enabled,
133 tombstoned: false,
134 deleted_at_ms: None,
135 created_at_ms,
136 updated_at_ms,
137 }
138}