Skip to main content

arkhe_kernel/runtime/
dispatch.rs

1//! Effect dispatcher — translate an `Effect<'i, Authorized>` into
2//! `StepStage` deltas.
3//!
4//! Inputs: an authorized Effect (Op + originating principal + branded
5//! instance), the running `StepStage`, current Tick, and the kernel's
6//! `next_scheduled_id` counter.
7//!
8//! Output: side-effect on `StepStage` only — no Instance mutation here;
9//! `runtime::apply::apply_stage` commits the stage post-dispatch.
10
11use crate::abi::{CapabilityMask, Principal, Tick};
12use crate::state::{Authorized, Effect, EntityMeta, Op, ScheduledActionId, ScheduledEntry};
13
14use super::event::KernelEvent;
15use super::stage::{LedgerOp, PendingSignal, ScheduledEntryDelta, StagedStateDelta, StepStage};
16
17/// Translate one authorized Effect into corresponding `StepStage` writes.
18///
19/// `next_scheduled_id` is monotonically advanced for `Op::ScheduleAction`
20/// to keep `ScheduledActionId` allocation deterministic across the step
21/// (id_counters tuple includes scheduled-id; staged so rollback
22/// rewinds it via `id_counters.next_scheduled_advance`).
23pub(crate) fn dispatch<'i>(
24    effect: Effect<'i, Authorized>,
25    stage: &mut StepStage,
26    now: Tick,
27    next_scheduled_id: &mut u64,
28    parent_caps: CapabilityMask,
29) {
30    let instance_id = effect.instance_id;
31    let effect_principal = match &effect.principal {
32        Principal::Unauthenticated => Principal::Unauthenticated,
33        Principal::External(e) => Principal::External(*e),
34        Principal::System => Principal::System,
35    };
36    match effect.op {
37        Op::SpawnEntity { id, owner } => {
38            stage.state_ops.push(StagedStateDelta::SpawnEntity {
39                id,
40                meta: EntityMeta {
41                    owner,
42                    created: now,
43                },
44            });
45            stage.ledger_delta.ops.push(LedgerOp::AddEntity(id));
46            stage.id_counters.next_entity_advance =
47                stage.id_counters.next_entity_advance.saturating_add(1);
48        }
49        Op::DespawnEntity { id } => {
50            stage.state_ops.push(StagedStateDelta::DespawnEntity { id });
51            stage.ledger_delta.ops.push(LedgerOp::RemoveEntity(id));
52        }
53        Op::SetComponent {
54            entity,
55            type_code,
56            bytes,
57            size,
58        } => {
59            stage.state_ops.push(StagedStateDelta::SetComponent {
60                entity,
61                type_code,
62                bytes,
63                size,
64            });
65            stage.ledger_delta.ops.push(LedgerOp::AddComponent {
66                entity,
67                type_code,
68                size,
69            });
70        }
71        Op::RemoveComponent {
72            entity,
73            type_code,
74            size,
75        } => {
76            stage.state_ops.push(StagedStateDelta::RemoveComponent {
77                entity,
78                type_code,
79                size,
80            });
81            stage.ledger_delta.ops.push(LedgerOp::RemoveComponent {
82                entity,
83                type_code,
84                size,
85            });
86        }
87        Op::EmitEvent {
88            actor,
89            event_type_code,
90            event_bytes,
91        } => {
92            stage.events.push_back(KernelEvent::DomainEventEmitted {
93                instance: instance_id,
94                actor,
95                event_type_code,
96                bytes: event_bytes,
97            });
98        }
99        Op::ScheduleAction {
100            at,
101            actor,
102            action_type_code,
103            action_bytes,
104        } => {
105            *next_scheduled_id = next_scheduled_id.saturating_add(1);
106            let id = ScheduledActionId::new(*next_scheduled_id)
107                .expect("next_scheduled_id incremented before use; non-zero");
108            stage
109                .schedule_deltas
110                .push(ScheduledEntryDelta::Add(ScheduledEntry {
111                    id,
112                    at,
113                    actor,
114                    // The scheduled child inherits the scheduling action's
115                    // principal and runs under a ceiling equal to the parent's
116                    // effective caps — privilege can only narrow, never widen
117                    // (no time-shifted escalation; action_principal is gone).
118                    principal: effect_principal,
119                    action_type_code,
120                    action_bytes: action_bytes.to_vec(),
121                    caps_ceiling: parent_caps,
122                }));
123            stage.id_counters.next_scheduled_advance =
124                stage.id_counters.next_scheduled_advance.saturating_add(1);
125        }
126        Op::SendSignal {
127            target,
128            route,
129            payload,
130        } => {
131            // Stage the outbound signal only. The in-flight refcount is NOT
132            // incremented here — it is credited at DELIVERY time in the apply
133            // router (a delivered signal increments; a dropped one does not),
134            // so the refcount is a conserved quantity, never a speculative
135            // dangling increment.
136            stage.pending_signals.push(PendingSignal {
137                target,
138                route,
139                payload,
140                principal: effect_principal,
141            });
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use bytes::Bytes;
150
151    use crate::abi::{CapabilityMask, EntityId, InstanceId, Principal, RouteId, Tick, TypeCode};
152    use crate::state::authz::authorize;
153    use crate::state::{Effect, Op, Unverified};
154
155    fn inst() -> InstanceId {
156        InstanceId::new(1).unwrap()
157    }
158    fn ent(n: u64) -> EntityId {
159        EntityId::new(n).unwrap()
160    }
161    fn caps() -> CapabilityMask {
162        CapabilityMask::all()
163    }
164
165    fn auth_system(op: Op) -> Effect<'static, crate::state::Authorized> {
166        let e: Effect<'static, Unverified> = Effect::new(inst(), Principal::System, op);
167        authorize(CapabilityMask::SYSTEM, e).expect("system always authorized")
168    }
169
170    #[test]
171    fn dispatch_spawn_entity_pushes_state_op_ledger_and_advances_counter() {
172        let mut stage = StepStage::default();
173        let mut next_id: u64 = 0;
174        let e = auth_system(Op::SpawnEntity {
175            id: ent(1),
176            owner: Principal::System,
177        });
178        dispatch(e, &mut stage, Tick(5), &mut next_id, caps());
179        assert_eq!(stage.state_ops.len(), 1);
180        assert_eq!(stage.ledger_delta.ops.len(), 1);
181        assert_eq!(stage.id_counters.next_entity_advance, 1);
182        assert_eq!(next_id, 0); // not advanced (no schedule)
183    }
184
185    #[test]
186    fn dispatch_despawn_entity_pushes_state_and_ledger_remove() {
187        let mut stage = StepStage::default();
188        let mut next_id: u64 = 0;
189        let e = auth_system(Op::DespawnEntity { id: ent(1) });
190        dispatch(e, &mut stage, Tick(0), &mut next_id, caps());
191        assert_eq!(stage.state_ops.len(), 1);
192        assert_eq!(stage.ledger_delta.ops.len(), 1);
193    }
194
195    #[test]
196    fn dispatch_set_component_pushes_state_and_ledger_add() {
197        let mut stage = StepStage::default();
198        let mut next_id: u64 = 0;
199        let e = auth_system(Op::SetComponent {
200            entity: ent(1),
201            type_code: TypeCode(7),
202            bytes: Bytes::from_static(b"data"),
203            size: 4,
204        });
205        dispatch(e, &mut stage, Tick(0), &mut next_id, caps());
206        assert_eq!(stage.state_ops.len(), 1);
207        assert_eq!(stage.ledger_delta.ops.len(), 1);
208    }
209
210    #[test]
211    fn dispatch_remove_component_pushes_state_and_ledger_remove() {
212        let mut stage = StepStage::default();
213        let mut next_id: u64 = 0;
214        let e = auth_system(Op::RemoveComponent {
215            entity: ent(1),
216            type_code: TypeCode(7),
217            size: 4,
218        });
219        dispatch(e, &mut stage, Tick(0), &mut next_id, caps());
220        assert_eq!(stage.state_ops.len(), 1);
221        assert_eq!(stage.ledger_delta.ops.len(), 1);
222    }
223
224    #[test]
225    fn dispatch_emit_event_pushes_kernel_event() {
226        let mut stage = StepStage::default();
227        let mut next_id: u64 = 0;
228        let e = auth_system(Op::EmitEvent {
229            actor: Some(ent(1)),
230            event_type_code: TypeCode(2),
231            event_bytes: Bytes::from_static(b"evt"),
232        });
233        dispatch(e, &mut stage, Tick(0), &mut next_id, caps());
234        assert_eq!(stage.events.len(), 1);
235        match stage.events.front().unwrap() {
236            KernelEvent::DomainEventEmitted {
237                event_type_code, ..
238            } => {
239                assert_eq!(*event_type_code, TypeCode(2));
240            }
241            _ => panic!("expected DomainEventEmitted"),
242        }
243    }
244
245    #[test]
246    fn dispatch_schedule_action_advances_id_and_pushes_delta() {
247        let mut stage = StepStage::default();
248        let mut next_id: u64 = 0;
249        let e = auth_system(Op::ScheduleAction {
250            at: Tick(10),
251            actor: None,
252            action_type_code: TypeCode(3),
253            action_bytes: Bytes::from_static(b"a"),
254        });
255        dispatch(e, &mut stage, Tick(0), &mut next_id, caps());
256        assert_eq!(stage.schedule_deltas.len(), 1);
257        assert_eq!(next_id, 1);
258        assert_eq!(stage.id_counters.next_scheduled_advance, 1);
259        // The scheduled child inherits the dispatching principal (System here)
260        // and the parent's caps ceiling — not a domain-supplied principal.
261        match &stage.schedule_deltas[0] {
262            ScheduledEntryDelta::Add(entry) => {
263                assert!(matches!(entry.principal, Principal::System));
264                assert_eq!(entry.caps_ceiling, caps());
265            }
266            ScheduledEntryDelta::Remove(_) => panic!("expected Add"),
267        }
268    }
269
270    #[test]
271    fn dispatch_send_signal_stages_pending_without_speculative_refcount() {
272        // The refcount is credited at delivery (apply router), NOT at dispatch
273        // — so dispatch stages only the pending signal. There is no staged
274        // refcount delta at all (the bucket was removed); a SendSignal can only
275        // produce a pending_signal, which closes the dangling-increment
276        // half-commit structurally.
277        let mut stage = StepStage::default();
278        let mut next_id: u64 = 0;
279        let route = RouteId(42);
280        let e = auth_system(Op::SendSignal {
281            target: inst(),
282            route,
283            payload: Bytes::from_static(b"hello"),
284        });
285        dispatch(e, &mut stage, Tick(0), &mut next_id, caps());
286        assert_eq!(stage.pending_signals.len(), 1);
287    }
288
289    #[test]
290    fn dispatch_send_signal_twice_stages_two_pending() {
291        let mut stage = StepStage::default();
292        let mut next_id: u64 = 0;
293        let route = RouteId(42);
294        for _ in 0..3 {
295            let e = auth_system(Op::SendSignal {
296                target: inst(),
297                route,
298                payload: Bytes::new(),
299            });
300            dispatch(e, &mut stage, Tick(0), &mut next_id, caps());
301        }
302        assert_eq!(stage.pending_signals.len(), 3);
303    }
304}