1use super::{
2 ActorKnowledge, Army, ArmyId, BoundaryId, BoundaryKnowledgeChange, CanwuError, CommandId,
3 CommandRecord, DomainRecord, DomainRecordKind, DomainRecordRef, DomainRecordType,
4 DomainRecordVersionRef, EntityRef, ErrorCode, EventId, EvidenceRef, Government, GovernmentId,
5 HashSet, IngressId, IngressPayload, IngressRecord, KnowledgeHolderRef, KnowledgeQuery,
6 KnowledgeRecord, KnowledgeRecordId, Person, PersonId, PluginComponentKey,
7 PluginComponentRecord, RandomOperationTarget, RandomStreamKey, RefCell, ReservationAllocation,
8 ReservationRef, Route, RouteId, RuntimeCurrentState, RuntimeEvidence, RuntimeState, SimEvent,
9 SimTime, StateKey, Territory, TerritoryId, TypedDomainRecordRef, Value, component_key, random,
10 records, validation,
11};
12use std::collections::{BTreeMap, BTreeSet};
13
14pub(super) enum SimulationViewState<'a> {
15 Runtime(&'a RuntimeState),
16 Boundary {
17 current: &'a RuntimeCurrentState,
18 now: SimTime,
19 runtime: &'a RuntimeState,
20 },
21}
22
23impl SimulationViewState<'_> {
24 const fn current(&self) -> &RuntimeCurrentState {
25 match self {
26 Self::Runtime(state) => &state.current,
27 Self::Boundary { current, .. } => current,
28 }
29 }
30
31 const fn now(&self) -> SimTime {
32 match self {
33 Self::Runtime(state) => state.scheduler.now,
34 Self::Boundary { now, .. } => *now,
35 }
36 }
37
38 const fn evidence(&self) -> &RuntimeEvidence {
39 match self {
40 Self::Runtime(state) => &state.evidence,
41 Self::Boundary { runtime, .. } => &runtime.evidence,
42 }
43 }
44
45 const fn runtime(&self) -> &RuntimeState {
46 match self {
47 Self::Runtime(state) | Self::Boundary { runtime: state, .. } => state,
48 }
49 }
50}
51
52pub struct SimulationView<'a> {
53 pub(super) state: SimulationViewState<'a>,
54 pub(super) state_owners: &'a BTreeMap<StateKey, String>,
55 pub(super) reader: Option<&'a str>,
56 pub(super) allowed_reads: Option<&'a [StateKey]>,
57 pub(super) allowed_ingress: Option<&'a HashSet<IngressId>>,
58 pub(super) ingress_plugin: Option<&'a str>,
59 pub(super) component_overlay: Option<&'a BTreeMap<PluginComponentKey, PluginComponentRecord>>,
60 pub(super) proposed_components: Option<&'a BTreeMap<PluginComponentKey, PluginComponentRecord>>,
61 pub(super) record_overlay: Option<&'a BTreeMap<DomainRecordRef, DomainRecord>>,
62 pub(super) proposed_records: Option<&'a BTreeMap<DomainRecordRef, DomainRecord>>,
63 pub(super) boundary_id: Option<BoundaryId>,
64 pub(super) proposal_evidence: Option<&'a BTreeSet<EvidenceRef>>,
65 pub(super) knowledge_overlay:
66 Option<&'a BTreeMap<KnowledgeHolderRef, BTreeMap<KnowledgeRecordId, KnowledgeRecord>>>,
67 pub(super) allocations: Option<&'a BTreeMap<ReservationRef, ReservationAllocation>>,
68 pub(super) allowed_reservations: Option<&'a [ReservationRef]>,
69 pub(super) random_session: Option<RefCell<random::RandomSession>>,
70}
71
72impl SimulationView<'_> {
73 #[must_use]
74 pub const fn time(&self) -> SimTime {
75 self.state.now()
76 }
77
78 pub fn army(&self, id: ArmyId) -> Result<Option<&Army>, CanwuError> {
79 self.require_read(&StateKey::core_armies())?;
80 Ok(self.state.current().armies.get(&id))
81 }
82
83 pub fn person(&self, id: PersonId) -> Result<Option<&Person>, CanwuError> {
84 self.require_read(&StateKey::core_people())?;
85 Ok(self.state.current().people.get(&id))
86 }
87
88 pub fn government(&self, id: GovernmentId) -> Result<Option<&Government>, CanwuError> {
89 self.require_read(&StateKey::core_governments())?;
90 Ok(self.state.current().governments.get(&id))
91 }
92
93 pub fn territory(&self, id: TerritoryId) -> Result<Option<&Territory>, CanwuError> {
94 self.require_read(&StateKey::core_territories())?;
95 Ok(self.state.current().territories.get(&id))
96 }
97
98 pub fn route(&self, id: RouteId) -> Result<Option<&Route>, CanwuError> {
99 self.require_read(&StateKey::core_routes())?;
100 Ok(self.state.current().routes.get(&id))
101 }
102
103 pub fn actor_knowledge(&self, actor: PersonId) -> Result<Option<&ActorKnowledge>, CanwuError> {
104 self.require_read(&StateKey::core_knowledge())?;
105 Ok(self.state.current().knowledge.for_actor(actor))
106 }
107
108 pub fn knowledge_records(
113 &self,
114 holder: KnowledgeHolderRef,
115 query: &KnowledgeQuery,
116 ) -> Result<canwu_knowledge::KnowledgeQueryResult, CanwuError> {
117 self.require_read(&StateKey::core_knowledge())?;
118 let result = if let Some(overlay) = self.knowledge_overlay {
119 self.state.current().knowledge.query_with_overlay(
120 holder,
121 query,
122 self.boundary_id,
123 overlay,
124 )
125 } else {
126 self.state
127 .current()
128 .knowledge
129 .query_current(holder, query, self.boundary_id)
130 };
131 result.map_err(|error| match error {
132 canwu_knowledge::KnowledgeQueryError::ReadCutUnavailable => CanwuError::new(
133 ErrorCode::KnowledgeReadCutUnavailable,
134 "knowledge cursor read cut is no longer available",
135 ),
136 canwu_knowledge::KnowledgeQueryError::InvalidLimit => CanwuError::new(
137 ErrorCode::KnowledgeLimitExceeded,
138 "knowledge query page size is outside the supported range",
139 ),
140 canwu_knowledge::KnowledgeQueryError::InvalidCursor
141 | canwu_knowledge::KnowledgeQueryError::InvalidLedger
142 | canwu_knowledge::KnowledgeQueryError::Encoding => CanwuError::new(
143 ErrorCode::InvalidKnowledgeRecord,
144 "knowledge query, cursor, or ledger is invalid",
145 ),
146 })
147 }
148
149 pub fn command(&self, id: CommandId) -> Result<Option<&CommandRecord>, CanwuError> {
156 self.require_read(&StateKey::core_commands())?;
157 let retained = self.state.evidence().retained_command(id);
158 if retained.is_none()
159 && self
160 .state
161 .evidence()
162 .archived_evidence_receipts
163 .contains_key(&EvidenceRef::Command(id))
164 {
165 return Err(CanwuError::new(
166 ErrorCode::EvidenceContentUnavailable,
167 "command identity is archived; payload inspection requires an archive provider",
168 ));
169 }
170 Ok(retained)
171 }
172
173 pub fn event(&self, id: EventId) -> Result<Option<&SimEvent>, CanwuError> {
180 self.require_read(&StateKey::core_events())?;
181 let retained = self.state.evidence().retained_event(id);
182 if retained.is_none()
183 && self
184 .state
185 .evidence()
186 .archived_evidence_receipts
187 .contains_key(&EvidenceRef::Event(id))
188 {
189 return Err(CanwuError::new(
190 ErrorCode::EvidenceContentUnavailable,
191 "event identity is archived; payload inspection requires an archive provider",
192 ));
193 }
194 Ok(retained)
195 }
196
197 pub fn ingress(&self, id: IngressId) -> Result<Option<&IngressRecord>, CanwuError> {
198 self.require_read(&StateKey::core_ingress())?;
199 if self
200 .allowed_ingress
201 .is_none_or(|allowed| !allowed.contains(&id))
202 {
203 return Ok(None);
204 }
205 let record = self.state.evidence().retained_ingress(id);
206 if record.is_none()
207 && self
208 .state
209 .evidence()
210 .archived_evidence_receipts
211 .contains_key(&EvidenceRef::Ingress(id))
212 {
213 return Err(CanwuError::new(
214 ErrorCode::EvidenceContentUnavailable,
215 "ingress identity is archived; payload inspection requires an archive provider",
216 ));
217 }
218 if let (Some(owner), Some(record)) = (self.ingress_plugin, record)
219 && !matches!(
220 &record.payload,
221 IngressPayload::Plugin { plugin, .. } if plugin == owner
222 )
223 {
224 return Ok(None);
225 }
226 Ok(record)
227 }
228
229 pub fn domain_record(
230 &self,
231 reference: &DomainRecordRef,
232 ) -> Result<Option<&DomainRecord>, CanwuError> {
233 self.require_read(&records::record_state_key(&reference.kind))?;
234 Ok(self
235 .record_overlay
236 .and_then(|overlay| overlay.get(reference))
237 .or_else(|| self.state.current().domain_records.get(reference)))
238 }
239
240 pub fn typed_domain_record<T: DomainRecordType>(
241 &self,
242 reference: &TypedDomainRecordRef<T>,
243 ) -> Result<Option<&DomainRecord>, CanwuError> {
244 self.domain_record(reference.as_untyped())
245 }
246
247 pub fn proposed_domain_record(
248 &self,
249 reference: &DomainRecordRef,
250 ) -> Result<Option<&DomainRecord>, CanwuError> {
251 self.require_read(&records::record_state_key(&reference.kind))?;
252 Ok(self
253 .proposed_records
254 .and_then(|records| records.get(reference)))
255 }
256
257 pub fn proposed_typed_domain_record<T: DomainRecordType>(
258 &self,
259 reference: &TypedDomainRecordRef<T>,
260 ) -> Result<Option<&DomainRecord>, CanwuError> {
261 self.proposed_domain_record(reference.as_untyped())
262 }
263
264 pub fn proposed_domain_record_version(
267 &self,
268 reference: &DomainRecordRef,
269 ) -> Result<Option<DomainRecordVersionRef>, CanwuError> {
270 self.require_read(&records::record_state_key(&reference.kind))?;
271 Ok(self.proposal_evidence.and_then(|evidence| {
272 evidence.iter().find_map(|item| match item {
273 EvidenceRef::DomainRecordVersion(version) if version.record == *reference => {
274 Some(version.clone())
275 }
276 _ => None,
277 })
278 }))
279 }
280
281 pub fn domain_record_version_evidence_exists(
288 &self,
289 reference: &DomainRecordVersionRef,
290 ) -> Result<bool, CanwuError> {
291 self.require_read(&records::record_state_key(&reference.record.kind))?;
292 if let Some(proposed) = self.proposed_domain_record_version(&reference.record)? {
293 return Ok(proposed == *reference);
294 }
295 Ok(!matches!(
296 validation::resolve_evidence_reference(
297 &validation::RuntimeValidationContext::new(self.state.runtime()),
298 &EvidenceRef::DomainRecordVersion(reference.clone()),
299 ),
300 validation::EvidenceAvailability::Missing
301 ))
302 }
303
304 pub fn domain_records_of_kind(
309 &self,
310 kind: &DomainRecordKind,
311 limit: usize,
312 ) -> Result<Vec<DomainRecord>, CanwuError> {
313 self.domain_records_of_kind_after(kind, None, limit)
314 }
315
316 pub fn domain_records_of_kind_after(
324 &self,
325 kind: &DomainRecordKind,
326 after: Option<&DomainRecordRef>,
327 limit: usize,
328 ) -> Result<Vec<DomainRecord>, CanwuError> {
329 const MAX_DOMAIN_RECORD_QUERY_LIMIT: usize = 10_000;
330 self.require_read(&records::record_state_key(kind))?;
331 if limit == 0 || limit > MAX_DOMAIN_RECORD_QUERY_LIMIT {
332 return Err(CanwuError::new(
333 ErrorCode::ValueOutOfRange,
334 format!(
335 "domain-record query limit must be between 1 and {MAX_DOMAIN_RECORD_QUERY_LIMIT}"
336 ),
337 ));
338 }
339 if after.is_some_and(|cursor| cursor.kind != *kind) {
340 return Err(CanwuError::new(
341 ErrorCode::InvalidPayload,
342 "domain-record page cursor has the wrong kind",
343 ));
344 }
345
346 let mut records = BTreeMap::new();
347 for (reference, record) in &self.state.current().domain_records {
348 if reference.kind == *kind {
349 records.insert(reference.clone(), record.clone());
350 }
351 }
352 for overlay in [self.record_overlay, self.proposed_records]
353 .into_iter()
354 .flatten()
355 {
356 for (reference, record) in overlay {
357 if reference.kind == *kind {
358 records.insert(reference.clone(), record.clone());
359 }
360 }
361 }
362 Ok(records
363 .into_iter()
364 .filter(|(reference, _)| after.is_none_or(|cursor| reference > cursor))
365 .map(|(_, record)| record)
366 .take(limit)
367 .collect())
368 }
369
370 pub fn knowledge_changes_by_correlation(
375 &self,
376 plugin: &str,
377 producer_correlation: &str,
378 ) -> Result<Vec<BoundaryKnowledgeChange>, CanwuError> {
379 self.require_read(&StateKey::core_knowledge())?;
380 Ok(self
381 .state
382 .evidence()
383 .boundaries
384 .iter()
385 .flat_map(|boundary| &boundary.knowledge_changes)
386 .filter(|change| {
387 change.plugin == plugin
388 && change.producer_correlation.as_deref() == Some(producer_correlation)
389 })
390 .cloned()
391 .collect())
392 }
393
394 pub fn knowledge_changes_by_correlation_prefix(
401 &self,
402 plugin: &str,
403 producer_correlation_prefix: &str,
404 ) -> Result<Vec<BoundaryKnowledgeChange>, CanwuError> {
405 self.require_read(&StateKey::core_knowledge())?;
406 Ok(self
407 .state
408 .evidence()
409 .boundaries
410 .iter()
411 .flat_map(|boundary| &boundary.knowledge_changes)
412 .filter(|change| {
413 change.plugin == plugin
414 && change
415 .producer_correlation
416 .as_deref()
417 .is_some_and(|value| value.starts_with(producer_correlation_prefix))
418 })
419 .cloned()
420 .collect())
421 }
422
423 pub fn reservation(
424 &self,
425 reservation: &ReservationRef,
426 ) -> Result<Option<&ReservationAllocation>, CanwuError> {
427 let reader = self.reader.unwrap_or("unscoped caller");
428 if self
429 .allowed_reservations
430 .is_none_or(|allowed| !allowed.contains(reservation))
431 {
432 return Err(CanwuError::new(
433 ErrorCode::UndeclaredStateRead,
434 format!(
435 "system {reader} did not declare reservation read {}.{}.{}",
436 reservation.plugin, reservation.system, reservation.request
437 ),
438 ));
439 }
440 Ok(self.allocations.and_then(|values| values.get(reservation)))
441 }
442
443 pub fn random_range(
444 &self,
445 stream: &RandomStreamKey,
446 upper_exclusive: u64,
447 purpose: &str,
448 ) -> Result<u64, CanwuError> {
449 let Some(session) = &self.random_session else {
450 return Err(CanwuError::new(
451 ErrorCode::UndeclaredRandomStream,
452 format!(
453 "system {} has no declared random streams",
454 self.reader.unwrap_or("unscoped caller")
455 ),
456 ));
457 };
458 session.borrow_mut().range(stream, upper_exclusive, purpose)
459 }
460
461 #[allow(clippy::too_many_arguments)]
462 pub fn random_range_for_operation(
463 &self,
464 stream: &RandomStreamKey,
465 evidence: EvidenceRef,
466 operation_kind: &str,
467 application_operation_id: &str,
468 target: RandomOperationTarget,
469 draw_slot: u32,
470 upper_exclusive: u64,
471 purpose: &str,
472 ) -> Result<u64, CanwuError> {
473 let available = self
474 .proposal_evidence
475 .is_some_and(|values| values.contains(&evidence))
476 || validation::resolve_evidence_reference(
477 &validation::RuntimeValidationContext::new(self.state.runtime()),
478 &evidence,
479 ) == validation::EvidenceAvailability::Retained;
480 if !available {
481 return Err(CanwuError::new(
482 ErrorCode::InvalidRandomOperationEvidence,
483 "operation-keyed random draw references unavailable evidence",
484 ));
485 }
486 let Some(session) = &self.random_session else {
487 return Err(CanwuError::new(
488 ErrorCode::UndeclaredRandomStream,
489 format!(
490 "system {} has no declared random streams",
491 self.reader.unwrap_or("unscoped caller")
492 ),
493 ));
494 };
495 session.borrow_mut().range_for_operation(
496 stream,
497 evidence,
498 operation_kind,
499 application_operation_id,
500 target,
501 draw_slot,
502 upper_exclusive,
503 purpose,
504 )
505 }
506
507 pub fn component(
508 &self,
509 state: &StateKey,
510 entity: &EntityRef,
511 component: &str,
512 ) -> Result<Option<&Value>, CanwuError> {
513 self.require_read(state)?;
514 let Some(owner) = self.state_owners.get(state) else {
515 return Err(CanwuError::new(
516 ErrorCode::UndeclaredStateRead,
517 format!(
518 "state {}.{} has no registered owner",
519 state.namespace, state.name
520 ),
521 ));
522 };
523 let key = component_key(owner, state, entity, component);
524 Ok(self
525 .component_overlay
526 .and_then(|overlay| overlay.get(&key))
527 .or_else(|| self.state.current().plugin_components.get(&key))
528 .map(|record| &record.value))
529 }
530
531 pub fn proposed_component(
532 &self,
533 state: &StateKey,
534 entity: &EntityRef,
535 component: &str,
536 ) -> Result<Option<&Value>, CanwuError> {
537 self.require_read(state)?;
538 let Some(owner) = self.state_owners.get(state) else {
539 return Err(CanwuError::new(
540 ErrorCode::UndeclaredStateRead,
541 format!(
542 "state {}.{} has no registered owner",
543 state.namespace, state.name
544 ),
545 ));
546 };
547 let key = component_key(owner, state, entity, component);
548 Ok(self
549 .proposed_components
550 .and_then(|proposals| proposals.get(&key))
551 .map(|record| &record.value))
552 }
553
554 fn require_read(&self, state: &StateKey) -> Result<(), CanwuError> {
555 if self
556 .allowed_reads
557 .is_some_and(|reads| !reads.contains(state))
558 {
559 return Err(CanwuError::new(
560 ErrorCode::UndeclaredStateRead,
561 format!(
562 "{} did not declare read access to {}.{}",
563 self.reader.unwrap_or("internal system"),
564 state.namespace,
565 state.name
566 ),
567 ));
568 }
569 Ok(())
570 }
571
572 pub(super) fn finish_random_session(self) -> Option<random::RandomExecution> {
573 self.random_session
574 .map(RefCell::into_inner)
575 .map(random::RandomSession::finish)
576 }
577}