1pub mod pairing;
24
25use crate::{
26 Snapshot, Value,
27 spatial::{RenderOutput, SpatialRuntime},
28};
29use serde::{Deserialize, Serialize};
30use std::collections::BTreeMap;
31use std::io::{self, Write};
32
33#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
36#[serde(deny_unknown_fields)]
37pub struct ProgramSource {
38 pub source: String,
40 pub modules: BTreeMap<String, String>,
42}
43
44#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
46#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
47#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
48pub enum LiveCommand {
49 Load {
51 source: String,
53 modules: BTreeMap<String, String>,
55 },
56 Step {
58 count: u32,
60 },
61 InvokeComponent {
63 #[cfg_attr(feature = "typescript", ts(type = "number"))]
65 entity: u64,
66 component: String,
68 field: String,
70 arguments: Vec<Value>,
72 },
73 InvokeEntity {
75 #[cfg_attr(feature = "typescript", ts(type = "number"))]
77 entity: u64,
78 method: String,
80 arguments: Vec<Value>,
82 },
83 Finish,
85}
86
87#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
90#[serde(deny_unknown_fields)]
91pub struct LiveEvent {
92 #[cfg_attr(feature = "typescript", ts(type = "number"))]
94 pub sequence: u64,
95 pub command: LiveCommand,
97}
98
99#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
101#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
102#[serde(deny_unknown_fields)]
103pub struct LiveHistory {
104 #[cfg_attr(feature = "typescript", ts(type = "number"))]
106 pub base_sequence: u64,
107 pub events: Vec<LiveEvent>,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
113#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
114#[serde(rename_all = "snake_case")]
115pub enum LiveRole {
116 Owner,
118 Participant,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
127#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
128#[serde(rename_all = "snake_case")]
129pub enum LiveDebugStatus {
130 Running,
132 Paused,
134 Finished,
136 Unavailable,
138}
139
140#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
145#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
146#[serde(deny_unknown_fields)]
147pub struct LiveDebugReport {
148 #[cfg_attr(feature = "typescript", ts(type = "number"))]
150 pub runtime_tick: u64,
151 #[cfg_attr(feature = "typescript", ts(type = "number"))]
153 pub presented_sequence: u64,
154 pub render_fps: Option<f32>,
156 pub status: LiveDebugStatus,
158}
159
160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
162#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
163#[serde(deny_unknown_fields)]
164pub struct LivePeer {
165 pub id: String,
167 pub name: String,
169 pub role: LiveRole,
171 #[cfg_attr(feature = "typescript", ts(type = "number | null"))]
173 pub sequence: Option<u64>,
174 pub debug: Option<LiveDebugReport>,
176}
177
178#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
181#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
182pub enum LiveClientMessage {
183 Hello {
185 token: String,
187 name: String,
189 },
190 Command {
192 request_id: String,
194 command: LiveCommand,
196 },
197 EditSource {
199 request_id: String,
201 #[cfg_attr(feature = "typescript", ts(type = "number"))]
203 base_revision: u64,
204 source: ProgramSource,
206 },
207 Ui {
209 request_id: String,
211 event: konjure_sdk::presentation::UiEvent,
213 },
214 Playback {
216 request_id: String,
218 playing: bool,
220 },
221 Ack {
223 #[cfg_attr(feature = "typescript", ts(type = "number"))]
225 sequence: u64,
226 },
227 Debug {
229 report: LiveDebugReport,
231 },
232 Close {
234 request_id: String,
236 },
237}
238
239#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
241#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
242#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
243pub enum LiveServerMessage {
244 Welcome {
246 role: LiveRole,
248 history: LiveHistory,
250 playing: bool,
252 peers: Vec<LivePeer>,
254 },
255 Event {
257 event: LiveEvent,
259 request_id: Option<String>,
261 },
262 Playback {
264 playing: bool,
266 },
267 Peers {
269 peers: Vec<LivePeer>,
271 },
272 Error {
274 message: String,
276 request_id: Option<String>,
278 },
279 Closed,
281}
282
283#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
285#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
286#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
287pub enum LiveError {
288 SourceLimit,
290 ArgumentsLimit,
292 TickLimit,
294 EventLimit,
296 JournalLimit,
298 Sequence {
300 #[cfg_attr(feature = "typescript", ts(type = "number"))]
302 expected: u64,
303 #[cfg_attr(feature = "typescript", ts(type = "number"))]
305 actual: u64,
306 },
307 SourceRevision {
309 #[cfg_attr(feature = "typescript", ts(type = "number"))]
311 expected: u64,
312 #[cfg_attr(feature = "typescript", ts(type = "number"))]
314 actual: u64,
315 },
316 Runtime {
318 diagnostics: Vec<LiveDiagnostic>,
320 },
321 NotLoaded,
323}
324
325impl std::fmt::Display for LiveError {
326 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 match self {
328 Self::SourceLimit => formatter.write_str("live source budget exceeded"),
329 Self::ArgumentsLimit => formatter.write_str("live argument budget exceeded"),
330 Self::TickLimit => formatter.write_str("live simulation tick budget exceeded"),
331 Self::EventLimit => formatter.write_str("live event budget exceeded"),
332 Self::JournalLimit => formatter.write_str("live journal budget exceeded"),
333 Self::NotLoaded => formatter.write_str("no live program is loaded"),
334 Self::Sequence { expected, actual } => {
335 write!(formatter, "expected event {expected}, received {actual}")
336 }
337 Self::SourceRevision { expected, actual } => write!(
338 formatter,
339 "source revision conflict: current {expected}, draft based on {actual}"
340 ),
341 Self::Runtime { diagnostics } => {
342 for (index, diagnostic) in diagnostics.iter().enumerate() {
343 if index > 0 {
344 formatter.write_str("; ")?;
345 }
346 write!(formatter, "{}: {}", diagnostic.code, diagnostic.message)?;
347 }
348 Ok(())
349 }
350 }
351 }
352}
353
354impl std::error::Error for LiveError {}
355
356#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
358#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
359#[serde(deny_unknown_fields)]
360pub struct LiveDiagnostic {
361 pub code: String,
363 pub message: String,
365}
366
367const MAX_EXACT_INTEGER: u64 = 9_007_199_254_740_991;
369
370pub const MAX_SOURCE_BYTES: usize = 1024 * 1024;
372pub const MAX_ARGUMENT_BYTES: usize = 64 * 1024;
374pub const MAX_EVENTS: usize = 100_000;
376pub const MAX_CUMULATIVE_TICKS: u64 = 216_000;
378pub const MAX_JOURNAL_BYTES: usize = 16 * 1024 * 1024;
380
381pub struct LiveRuntime {
383 runtime: Option<SpatialRuntime>,
384 sequence: u64,
385 source_revision: u64,
386 history: LiveHistory,
387 cumulative_ticks: u64,
388 journal_bytes: usize,
389}
390
391impl Default for LiveRuntime {
392 fn default() -> Self {
393 Self::new()
394 }
395}
396
397impl LiveRuntime {
398 #[must_use]
400 pub fn new() -> Self {
401 Self {
402 runtime: None,
403 sequence: 0,
404 source_revision: 0,
405 history: LiveHistory::default(),
406 cumulative_ticks: 0,
407 journal_bytes: history_envelope_bytes(0),
408 }
409 }
410
411 pub fn apply(&mut self, command: LiveCommand) -> Result<LiveEvent, LiveError> {
417 self.apply_next(command)
418 }
419
420 pub fn accept(&mut self, event: LiveEvent) -> Result<(), LiveError> {
425 let expected = self.sequence.checked_add(1).ok_or(LiveError::EventLimit)?;
426 if event.sequence != expected {
427 return Err(LiveError::Sequence {
428 expected,
429 actual: event.sequence,
430 });
431 }
432 self.apply_next(event.command)?;
433 Ok(())
434 }
435
436 pub fn restore(&mut self, history: LiveHistory) -> Result<(), LiveError> {
441 validate_history(&history)?;
442 let mut candidate = Self {
443 sequence: history.base_sequence,
444 history: LiveHistory {
445 base_sequence: history.base_sequence,
446 events: Vec::new(),
447 },
448 journal_bytes: history_envelope_bytes(history.base_sequence),
449 ..Self::new()
450 };
451 for event in history.events {
452 candidate.accept(event)?;
453 }
454 *self = candidate;
455 Ok(())
456 }
457
458 #[must_use]
460 pub fn snapshot(&self) -> Option<Snapshot> {
461 self.runtime.as_ref().map(SpatialRuntime::snapshot)
462 }
463
464 #[must_use]
467 pub fn execution_state(&self) -> Option<(u64, bool)> {
468 self.runtime.as_ref().map(SpatialRuntime::execution_state)
469 }
470
471 pub fn render(&self) -> Result<RenderOutput, LiveError> {
473 let mut output = self
474 .runtime
475 .as_ref()
476 .ok_or(LiveError::NotLoaded)?
477 .render()
478 .map_err(runtime_error)?;
479 output.ui.revision = self.source_revision();
480 Ok(output)
481 }
482
483 #[must_use]
485 pub fn history(&self) -> &LiveHistory {
486 &self.history
487 }
488
489 #[must_use]
491 pub const fn sequence(&self) -> u64 {
492 self.sequence
493 }
494
495 #[must_use]
500 pub const fn source_revision(&self) -> u64 {
501 self.source_revision
502 }
503
504 pub fn edit_source(
510 &mut self,
511 base_revision: u64,
512 source: ProgramSource,
513 ) -> Result<LiveEvent, LiveError> {
514 if base_revision != self.source_revision {
515 return Err(LiveError::SourceRevision {
516 expected: self.source_revision,
517 actual: base_revision,
518 });
519 }
520 self.apply(LiveCommand::Load {
521 source: source.source,
522 modules: source.modules,
523 })
524 }
525
526 fn apply_next(&mut self, command: LiveCommand) -> Result<LiveEvent, LiveError> {
527 validate_command(&command)?;
528 let event = LiveEvent {
529 sequence: self
530 .sequence
531 .checked_add(1)
532 .filter(|value| *value <= MAX_EXACT_INTEGER)
533 .ok_or(LiveError::EventLimit)?,
534 command,
535 };
536 let event_bytes = encoded_len(&event, MAX_JOURNAL_BYTES)?;
537 let (base_sequence, journal_bytes, resets_history) =
538 if matches!(event.command, LiveCommand::Load { .. }) {
539 let base_sequence = self.sequence;
540 (
541 base_sequence,
542 history_envelope_bytes(base_sequence)
543 .checked_add(event_bytes)
544 .ok_or(LiveError::JournalLimit)?,
545 true,
546 )
547 } else {
548 if self.history.events.len() >= MAX_EVENTS {
549 return Err(LiveError::EventLimit);
550 }
551 let separator = usize::from(!self.history.events.is_empty());
552 (
553 self.history.base_sequence,
554 self.journal_bytes
555 .checked_add(separator)
556 .and_then(|bytes| bytes.checked_add(event_bytes))
557 .ok_or(LiveError::JournalLimit)?,
558 false,
559 )
560 };
561 if journal_bytes > MAX_JOURNAL_BYTES {
562 return Err(LiveError::JournalLimit);
563 }
564 self.execute(&event.command)?;
565 if resets_history {
566 self.history = LiveHistory {
567 base_sequence,
568 events: vec![event.clone()],
569 }
570 } else {
571 self.history.events.push(event.clone());
572 }
573 self.sequence = event.sequence;
574 if matches!(event.command, LiveCommand::Load { .. }) {
575 self.source_revision = event.sequence;
576 }
577 self.journal_bytes = journal_bytes;
578 Ok(event)
579 }
580
581 fn execute(&mut self, command: &LiveCommand) -> Result<(), LiveError> {
582 match command {
583 LiveCommand::Load { source, modules } => {
584 self.runtime = Some(SpatialRuntime::new(source, modules).map_err(runtime_errors)?);
585 self.cumulative_ticks = 0;
586 }
587 LiveCommand::Step { count } => {
588 let ticks = self
589 .cumulative_ticks
590 .checked_add(u64::from(*count))
591 .ok_or(LiveError::TickLimit)?;
592 if ticks > MAX_CUMULATIVE_TICKS {
593 return Err(LiveError::TickLimit);
594 }
595 self.runtime
596 .as_mut()
597 .ok_or(LiveError::NotLoaded)?
598 .step(*count)
599 .map_err(runtime_error)?;
600 self.cumulative_ticks = ticks;
601 }
602 LiveCommand::InvokeComponent {
603 entity,
604 component,
605 field,
606 arguments,
607 } => {
608 self.runtime
609 .as_mut()
610 .ok_or(LiveError::NotLoaded)?
611 .invoke_component(*entity, component, field, arguments)
612 .map_err(runtime_error)?;
613 }
614 LiveCommand::InvokeEntity {
615 entity,
616 method,
617 arguments,
618 } => {
619 self.runtime
620 .as_mut()
621 .ok_or(LiveError::NotLoaded)?
622 .invoke_entity(*entity, method, arguments)
623 .map_err(runtime_error)?;
624 }
625 LiveCommand::Finish => {
626 self.runtime
627 .as_mut()
628 .ok_or(LiveError::NotLoaded)?
629 .finish()
630 .map_err(runtime_error)?;
631 }
632 }
633 Ok(())
634 }
635}
636
637fn validate_command(command: &LiveCommand) -> Result<(), LiveError> {
638 match command {
639 LiveCommand::Load { source, modules } => {
640 let size = modules
641 .iter()
642 .try_fold(source.len(), |total, (name, value)| {
643 total.checked_add(name.len())?.checked_add(value.len())
644 })
645 .ok_or(LiveError::SourceLimit)?;
646 if size > MAX_SOURCE_BYTES {
647 return Err(LiveError::SourceLimit);
648 }
649 }
650 LiveCommand::InvokeComponent {
651 entity, arguments, ..
652 }
653 | LiveCommand::InvokeEntity {
654 entity, arguments, ..
655 } => {
656 if *entity == 0 || *entity > MAX_EXACT_INTEGER {
657 return Err(LiveError::ArgumentsLimit);
658 }
659 let argument_bytes = encoded_len(arguments, MAX_ARGUMENT_BYTES)
660 .map_err(|_| LiveError::ArgumentsLimit)?;
661 if argument_bytes > MAX_ARGUMENT_BYTES {
662 return Err(LiveError::ArgumentsLimit);
663 }
664 }
665 LiveCommand::Step { .. } | LiveCommand::Finish => {}
666 }
667 Ok(())
668}
669
670fn validate_history(history: &LiveHistory) -> Result<(), LiveError> {
671 if history.base_sequence > MAX_EXACT_INTEGER {
672 return Err(LiveError::EventLimit);
673 }
674 if history.events.len() > MAX_EVENTS {
675 return Err(LiveError::EventLimit);
676 }
677 if encoded_len(history, MAX_JOURNAL_BYTES)? > MAX_JOURNAL_BYTES {
678 return Err(LiveError::JournalLimit);
679 }
680 let mut expected = history.base_sequence;
681 let mut cumulative_ticks = 0_u64;
682 for event in &history.events {
683 expected = expected
684 .checked_add(1)
685 .filter(|value| *value <= MAX_EXACT_INTEGER)
686 .ok_or(LiveError::EventLimit)?;
687 if event.sequence != expected {
688 return Err(LiveError::Sequence {
689 expected,
690 actual: event.sequence,
691 });
692 }
693 validate_command(&event.command)?;
694 match &event.command {
695 LiveCommand::Load { .. } => cumulative_ticks = 0,
696 LiveCommand::Step { count } => {
697 cumulative_ticks = cumulative_ticks
698 .checked_add(u64::from(*count))
699 .ok_or(LiveError::TickLimit)?;
700 if cumulative_ticks > MAX_CUMULATIVE_TICKS {
701 return Err(LiveError::TickLimit);
702 }
703 }
704 LiveCommand::InvokeComponent { .. }
705 | LiveCommand::InvokeEntity { .. }
706 | LiveCommand::Finish => {}
707 }
708 }
709 Ok(())
710}
711
712fn history_envelope_bytes(base_sequence: u64) -> usize {
713 serde_json::to_vec(&LiveHistory {
714 base_sequence,
715 events: Vec::new(),
716 })
717 .expect("live history envelope is serializable")
718 .len()
719}
720
721fn encoded_len(value: &impl Serialize, limit: usize) -> Result<usize, LiveError> {
722 let mut writer = LimitedWriter { written: 0, limit };
723 serde_json::to_writer(&mut writer, value).map_err(|_| LiveError::JournalLimit)?;
724 Ok(writer.written)
725}
726
727struct LimitedWriter {
728 written: usize,
729 limit: usize,
730}
731
732impl Write for LimitedWriter {
733 fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
734 let remaining = self.limit.saturating_sub(self.written);
735 if buffer.len() > remaining {
736 return Err(io::Error::new(
737 io::ErrorKind::WriteZero,
738 "live journal exceeds limit",
739 ));
740 }
741 self.written += buffer.len();
742 Ok(buffer.len())
743 }
744
745 fn flush(&mut self) -> io::Result<()> {
746 Ok(())
747 }
748}
749
750fn runtime_error(diagnostic: crate::Diagnostic) -> LiveError {
751 LiveError::Runtime {
752 diagnostics: vec![LiveDiagnostic {
753 code: diagnostic.code,
754 message: diagnostic.message,
755 }],
756 }
757}
758
759fn runtime_errors(diagnostics: Vec<crate::Diagnostic>) -> LiveError {
760 LiveError::Runtime {
761 diagnostics: diagnostics
762 .into_iter()
763 .map(|diagnostic| LiveDiagnostic {
764 code: diagnostic.code,
765 message: diagnostic.message,
766 })
767 .collect(),
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774
775 const SOURCE: &str = include_str!("../examples/lifecycle.kj");
776
777 fn load(runtime: &mut LiveRuntime) {
778 runtime
779 .apply(LiveCommand::Load {
780 source: SOURCE.into(),
781 modules: BTreeMap::new(),
782 })
783 .unwrap();
784 }
785
786 #[test]
787 fn journal_replay_produces_the_same_snapshot_and_render() {
788 let mut original = LiveRuntime::new();
789 assert_eq!(original.execution_state(), None);
790 load(&mut original);
791 assert_eq!(original.execution_state(), Some((0, false)));
792 original.apply(LiveCommand::Step { count: 2 }).unwrap();
793 assert_eq!(original.execution_state(), Some((2, false)));
794 original.apply(LiveCommand::Finish).unwrap();
795 assert_eq!(original.execution_state(), Some((2, true)));
796 let history = original.history().clone();
797 let snapshot = original.snapshot();
798 let render = serde_json::to_value(original.render().unwrap()).unwrap();
799
800 let mut replay = LiveRuntime::new();
801 replay.restore(history).unwrap();
802
803 assert_eq!(replay.snapshot(), snapshot);
804 assert_eq!(
805 serde_json::to_value(replay.render().unwrap()).unwrap(),
806 render
807 );
808 }
809
810 #[test]
811 fn rejected_load_action_and_sequence_preserve_accepted_state() {
812 let mut runtime = LiveRuntime::new();
813 load(&mut runtime);
814 let before = runtime.snapshot();
815 let history = runtime.history().clone();
816 let sequence = runtime.sequence();
817
818 assert!(matches!(
819 runtime.apply(LiveCommand::Load {
820 source: "x".repeat(MAX_SOURCE_BYTES + 1),
821 modules: BTreeMap::new(),
822 }),
823 Err(LiveError::SourceLimit)
824 ));
825 assert!(matches!(
826 runtime.apply(LiveCommand::InvokeEntity {
827 entity: 999,
828 method: "missing".into(),
829 arguments: Vec::new(),
830 }),
831 Err(LiveError::Runtime { .. })
832 ));
833 assert!(matches!(
834 runtime.accept(LiveEvent {
835 sequence: sequence + 2,
836 command: LiveCommand::Step { count: 1 },
837 }),
838 Err(LiveError::Sequence { .. })
839 ));
840
841 assert_eq!(runtime.snapshot(), before);
842 assert_eq!(runtime.history(), &history);
843 assert_eq!(runtime.sequence(), sequence);
844 }
845
846 #[test]
847 fn source_edits_use_the_last_load_sequence_not_the_latest_event() {
848 let mut runtime = LiveRuntime::new();
849 load(&mut runtime);
850 let base_revision = runtime.source_revision();
851 runtime.apply(LiveCommand::Step { count: 2 }).unwrap();
852 assert_eq!(runtime.source_revision(), base_revision);
853
854 let first = runtime
855 .edit_source(
856 base_revision,
857 ProgramSource {
858 source: SOURCE.into(),
859 modules: BTreeMap::new(),
860 },
861 )
862 .unwrap();
863 assert_eq!(runtime.source_revision(), first.sequence);
864 assert!(matches!(
865 runtime.edit_source(
866 base_revision,
867 ProgramSource {
868 source: SOURCE.into(),
869 modules: BTreeMap::new(),
870 },
871 ),
872 Err(LiveError::SourceRevision {
873 expected,
874 actual,
875 }) if expected == first.sequence && actual == base_revision
876 ));
877 }
878
879 #[test]
880 fn invalid_source_edit_preserves_runtime_and_source_revision() {
881 let mut runtime = LiveRuntime::new();
882 load(&mut runtime);
883 runtime.apply(LiveCommand::Step { count: 1 }).unwrap();
884 let before_snapshot = runtime.snapshot();
885 let before_history = runtime.history().clone();
886 let before_sequence = runtime.sequence();
887 let before_revision = runtime.source_revision();
888
889 assert!(matches!(
890 runtime.edit_source(
891 before_revision,
892 ProgramSource {
893 source: "not valid Konjure".into(),
894 modules: BTreeMap::new(),
895 },
896 ),
897 Err(LiveError::Runtime { .. })
898 ));
899 assert_eq!(runtime.snapshot(), before_snapshot);
900 assert_eq!(runtime.history(), &before_history);
901 assert_eq!(runtime.sequence(), before_sequence);
902 assert_eq!(runtime.source_revision(), before_revision);
903 }
904
905 #[test]
906 fn caps_reject_replay_and_cumulative_ticks_without_mutation() {
907 let mut runtime = LiveRuntime::new();
908 load(&mut runtime);
909 runtime.cumulative_ticks = MAX_CUMULATIVE_TICKS - 1;
910 let before = runtime.snapshot();
911 assert_eq!(
912 runtime.apply(LiveCommand::Step { count: 2 }),
913 Err(LiveError::TickLimit)
914 );
915 assert_eq!(runtime.snapshot(), before);
916
917 let oversized = LiveHistory {
918 base_sequence: 0,
919 events: vec![LiveEvent {
920 sequence: 1,
921 command: LiveCommand::Load {
922 source: "x".repeat(MAX_SOURCE_BYTES + 1),
923 modules: BTreeMap::new(),
924 },
925 }],
926 };
927 assert_eq!(runtime.restore(oversized), Err(LiveError::SourceLimit));
928 assert_eq!(runtime.snapshot(), before);
929
930 let too_many_events = LiveHistory {
931 base_sequence: 0,
932 events: vec![
933 LiveEvent {
934 sequence: 1,
935 command: LiveCommand::Step { count: 0 },
936 };
937 MAX_EVENTS + 1
938 ],
939 };
940 assert_eq!(runtime.restore(too_many_events), Err(LiveError::EventLimit));
941 assert_eq!(runtime.snapshot(), before);
942 }
943
944 #[test]
945 fn cached_journal_size_matches_serialized_history_after_append_restore_and_load() {
946 let mut runtime = LiveRuntime::new();
947 let check = |runtime: &LiveRuntime| {
948 assert_eq!(
949 runtime.journal_bytes,
950 serde_json::to_vec(runtime.history()).unwrap().len()
951 );
952 };
953 check(&runtime);
954 load(&mut runtime);
955 check(&runtime);
956 for _ in 0..12 {
957 runtime.apply(LiveCommand::Step { count: 1 }).unwrap();
958 check(&runtime);
959 }
960 let mut restored = LiveRuntime::new();
961 restored.restore(runtime.history().clone()).unwrap();
962 check(&restored);
963 load(&mut restored);
964 check(&restored);
965 }
966
967 #[test]
968 fn finish_runs_lifecycle_once_even_when_the_command_repeats() {
969 let mut runtime = LiveRuntime::new();
970 load(&mut runtime);
971 runtime.apply(LiveCommand::Step { count: 2 }).unwrap();
972 runtime.apply(LiveCommand::Finish).unwrap();
973 runtime.apply(LiveCommand::Finish).unwrap();
974 let snapshot = runtime.snapshot().unwrap();
975 assert!(snapshot.finished);
976 assert_eq!(snapshot.logs, ["Started", "Finished"]);
977 }
978}