Skip to main content

konjure_lang/
live.rs

1//! Deterministic, replayable command journal for a live language session.
2//!
3//! The journal is an in-memory protocol. It never reads files, invokes host
4//! registries, or repeats external effects while replaying accepted commands.
5//!
6//! ```
7//! use konjure_lang::{LiveCommand, LiveRuntime};
8//! use std::collections::BTreeMap;
9//!
10//! let mut session = LiveRuntime::new();
11//! let event = session.apply(LiveCommand::Load {
12//!     source: "let ball = spawn(Sphere { radius: 1 });".into(),
13//!     modules: BTreeMap::new(),
14//! })?;
15//! assert_eq!(event.sequence, 1);
16//! let history = session.history().clone();
17//! let mut replay = LiveRuntime::new();
18//! replay.restore(history)?;
19//! assert_eq!(replay.snapshot(), session.snapshot());
20//! # Ok::<(), konjure_lang::LiveError>(())
21//! ```
22
23pub 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/// Source text and explicitly supplied modules for one language revision.
34#[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    /// Main module source text.
39    pub source: String,
40    /// Imported module source, keyed by its explicit module name.
41    pub modules: BTreeMap<String, String>,
42}
43
44/// An ordered, serializable state change accepted by a live language session.
45#[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    /// Compile and initialize a new source revision.
50    Load {
51        /// Main module source text.
52        source: String,
53        /// Explicitly supplied imported modules.
54        modules: BTreeMap<String, String>,
55    },
56    /// Advance fixed simulation ticks.
57    Step {
58        /// Fixed ticks to advance.
59        count: u32,
60    },
61    /// Invoke a callback stored in a component field.
62    InvokeComponent {
63        /// Stable entity identity.
64        #[cfg_attr(feature = "typescript", ts(type = "number"))]
65        entity: u64,
66        /// Nominal component key.
67        component: String,
68        /// Callback field name.
69        field: String,
70        /// Typed callback arguments.
71        arguments: Vec<Value>,
72    },
73    /// Invoke a typed method on a live entity.
74    InvokeEntity {
75        /// Stable entity identity.
76        #[cfg_attr(feature = "typescript", ts(type = "number"))]
77        entity: u64,
78        /// Typed method name.
79        method: String,
80        /// Typed method arguments.
81        arguments: Vec<Value>,
82    },
83    /// Run terminal lifecycle callbacks once.
84    Finish,
85}
86
87/// A command with its globally monotonic session sequence number.
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
90#[serde(deny_unknown_fields)]
91pub struct LiveEvent {
92    /// Sequence assigned when the command was accepted.
93    #[cfg_attr(feature = "typescript", ts(type = "number"))]
94    pub sequence: u64,
95    /// The accepted command.
96    pub command: LiveCommand,
97}
98
99/// A bounded retained suffix of a live session's event stream.
100#[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    /// Sequence immediately before the first retained event.
105    #[cfg_attr(feature = "typescript", ts(type = "number"))]
106    pub base_sequence: u64,
107    /// Accepted events in exact sequence order.
108    pub events: Vec<LiveEvent>,
109}
110
111/// Authority granted to one connected live-session peer.
112#[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    /// May submit state-changing commands.
117    Owner,
118    /// May receive and replay accepted events.
119    Participant,
120}
121
122/// Runtime state reported by a connected live-session client.
123///
124/// This is observational telemetry only. It never changes the accepted command
125/// journal, playback state, or generated scene.
126#[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    /// The client is advancing an unfinished runtime.
131    Running,
132    /// The client has a loaded runtime that is not advancing.
133    Paused,
134    /// The client has run its terminal lifecycle callbacks.
135    Finished,
136    /// The client can report transport progress but not runtime state.
137    Unavailable,
138}
139
140/// Bounded runtime and render telemetry supplied by one connected client.
141///
142/// Hosts validate this report before retaining it. It is not a debugger and
143/// does not carry source, traces, logs, commands, or scene state.
144#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
145#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
146#[serde(deny_unknown_fields)]
147pub struct LiveDebugReport {
148    /// The client's accepted runtime tick, when it can observe one.
149    #[cfg_attr(feature = "typescript", ts(type = "number"))]
150    pub runtime_tick: u64,
151    /// The latest command sequence submitted to that client's renderer.
152    #[cfg_attr(feature = "typescript", ts(type = "number"))]
153    pub presented_sequence: u64,
154    /// Recent local render rate, if the client measures one.
155    pub render_fps: Option<f32>,
156    /// The extent to which this client can observe its runtime.
157    pub status: LiveDebugStatus,
158}
159
160/// Public progress information for one live-session connection.
161#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
162#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
163#[serde(deny_unknown_fields)]
164pub struct LivePeer {
165    /// Host-assigned stable connection identifier.
166    pub id: String,
167    /// Display name supplied during hello.
168    pub name: String,
169    /// Authority assigned by the host, independent of the client-supplied name.
170    pub role: LiveRole,
171    /// Last known accepted event sequence, when the peer has loaded a revision.
172    #[cfg_attr(feature = "typescript", ts(type = "number | null"))]
173    pub sequence: Option<u64>,
174    /// The latest admitted runtime and render telemetry for this connection.
175    pub debug: Option<LiveDebugReport>,
176}
177
178/// Client-to-host live-session wire message.
179#[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    /// Authenticates and names a new connection.
184    Hello {
185        /// Session access token.
186        token: String,
187        /// Human-readable connection name.
188        name: String,
189    },
190    /// Requests an owner-authorized live command.
191    Command {
192        /// Client-scoped identifier echoed by the host response.
193        request_id: String,
194        /// Requested runtime command.
195        command: LiveCommand,
196    },
197    /// Replaces the program only when the caller based it on the current source revision.
198    EditSource {
199        /// Client-scoped identifier echoed by the host response.
200        request_id: String,
201        /// Sequence of the last accepted [`LiveCommand::Load`], not a simulation tick.
202        #[cfg_attr(feature = "typescript", ts(type = "number"))]
203        base_revision: u64,
204        /// Complete source replacement candidate.
205        source: ProgramSource,
206    },
207    /// Requests a checked semantic UI action from an authenticated peer.
208    Ui {
209        /// Client-scoped identifier echoed by the host response.
210        request_id: String,
211        /// Event emitted against the view's displayed semantic UI revision.
212        event: konjure_sdk::presentation::UiEvent,
213    },
214    /// Starts or stops host-coordinated playback.
215    Playback {
216        /// Client-scoped identifier echoed by the host response.
217        request_id: String,
218        /// Whether playback should advance.
219        playing: bool,
220    },
221    /// Reports the latest event sequence applied by this client.
222    Ack {
223        /// Last exact event sequence applied.
224        #[cfg_attr(feature = "typescript", ts(type = "number"))]
225        sequence: u64,
226    },
227    /// Publishes bounded runtime and render telemetry without changing the session.
228    Debug {
229        /// Latest client-local runtime and render progress.
230        report: LiveDebugReport,
231    },
232    /// Requests a clean connection close.
233    Close {
234        /// Client-scoped identifier echoed by the close response.
235        request_id: String,
236    },
237}
238
239/// Host-to-client live-session wire message.
240#[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    /// Establishes role, retained journal, playback state, and peers.
245    Welcome {
246        /// Authority assigned to this connection.
247        role: LiveRole,
248        /// Replayable retained journal suffix.
249        history: LiveHistory,
250        /// Current host playback state.
251        playing: bool,
252        /// Visible connected peers.
253        peers: Vec<LivePeer>,
254    },
255    /// Broadcasts one accepted event, optionally replying to a request.
256    Event {
257        /// Accepted runtime event.
258        event: LiveEvent,
259        /// Originating client request, when applicable.
260        request_id: Option<String>,
261    },
262    /// Broadcasts a playback state change.
263    Playback {
264        /// Current host playback state.
265        playing: bool,
266    },
267    /// Broadcasts the visible peer list.
268    Peers {
269        /// Connected peers and reported progress.
270        peers: Vec<LivePeer>,
271    },
272    /// Reports a rejected request without changing accepted runtime state.
273    Error {
274        /// Human-readable rejection reason.
275        message: String,
276        /// Originating client request, when applicable.
277        request_id: Option<String>,
278    },
279    /// Confirms connection closure.
280    Closed,
281}
282
283/// A rejected live command or replay history. Rejections preserve committed state.
284#[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    /// Source plus imported modules exceeds the revision input budget.
289    SourceLimit,
290    /// Encoded callback or method arguments exceed the command budget.
291    ArgumentsLimit,
292    /// A session exceeds its total fixed-tick budget.
293    TickLimit,
294    /// The retained event count has reached its fixed bound.
295    EventLimit,
296    /// The retained journal cannot fit its fixed byte budget.
297    JournalLimit,
298    /// Replay input was not the next exact sequence number.
299    Sequence {
300        /// Required next global sequence number.
301        #[cfg_attr(feature = "typescript", ts(type = "number"))]
302        expected: u64,
303        /// Sequence supplied by the rejected event.
304        #[cfg_attr(feature = "typescript", ts(type = "number"))]
305        actual: u64,
306    },
307    /// A source replacement was based on a different accepted source revision.
308    SourceRevision {
309        /// The current accepted source revision.
310        #[cfg_attr(feature = "typescript", ts(type = "number"))]
311        expected: u64,
312        /// The source revision supplied by the editor.
313        #[cfg_attr(feature = "typescript", ts(type = "number"))]
314        actual: u64,
315    },
316    /// A runtime compile, validation, or lifecycle action failed.
317    Runtime {
318        /// Compact compiler or runtime error details for protocol consumers.
319        diagnostics: Vec<LiveDiagnostic>,
320    },
321    /// A state-dependent operation was requested before a source revision loaded.
322    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/// Serializable diagnostic detail carried by [`LiveError::Runtime`].
357#[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    /// Stable language diagnostic code.
362    pub code: String,
363    /// Human-readable diagnostic message.
364    pub message: String,
365}
366
367/// Largest integer shared exactly by the Rust and JavaScript wire formats.
368const MAX_EXACT_INTEGER: u64 = 9_007_199_254_740_991;
369
370/// Maximum combined UTF-8 source and module bytes in one revision.
371pub const MAX_SOURCE_BYTES: usize = 1024 * 1024;
372/// Maximum serialized arguments in one invocation command.
373pub const MAX_ARGUMENT_BYTES: usize = 64 * 1024;
374/// Maximum events retained in one journal suffix.
375pub const MAX_EVENTS: usize = 100_000;
376/// Maximum fixed ticks accepted across one loaded revision.
377pub const MAX_CUMULATIVE_TICKS: u64 = 216_000;
378/// Maximum serialized retained journal bytes.
379pub const MAX_JOURNAL_BYTES: usize = 16 * 1024 * 1024;
380
381/// An explicitly stepped language runtime with a bounded, replayable journal.
382pub 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    /// Creates an empty session. The first accepted command must be [`LiveCommand::Load`].
399    #[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    /// Applies a command transactionally and appends its exact event to the journal.
412    ///
413    /// # Errors
414    /// Rejects oversized inputs, invalid runtime actions, or a full journal without
415    /// changing the accepted runtime, sequence, or history.
416    pub fn apply(&mut self, command: LiveCommand) -> Result<LiveEvent, LiveError> {
417        self.apply_next(command)
418    }
419
420    /// Accepts one replay event only when its sequence is exactly next.
421    ///
422    /// # Errors
423    /// Rejects gaps, duplicates, and invalid commands without changing this session.
424    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    /// Replays a complete retained history transactionally, then replaces this session.
437    ///
438    /// # Errors
439    /// Rejects non-contiguous, oversized, or invalid histories while preserving state.
440    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    /// Returns the accepted language state, if a revision has loaded.
459    #[must_use]
460    pub fn snapshot(&self) -> Option<Snapshot> {
461        self.runtime.as_ref().map(SpatialRuntime::snapshot)
462    }
463
464    /// Returns the accepted runtime tick and terminal lifecycle state without
465    /// cloning the scene or snapshot.
466    #[must_use]
467    pub fn execution_state(&self) -> Option<(u64, bool)> {
468        self.runtime.as_ref().map(SpatialRuntime::execution_state)
469    }
470
471    /// Returns validated generated render data for the accepted language state.
472    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    /// Returns the bounded retained journal suffix.
484    #[must_use]
485    pub fn history(&self) -> &LiveHistory {
486        &self.history
487    }
488
489    /// Returns the last globally assigned session sequence number.
490    #[must_use]
491    pub const fn sequence(&self) -> u64 {
492        self.sequence
493    }
494
495    /// Returns the sequence of the last accepted source load.
496    ///
497    /// Simulation ticks and invocations do not change this revision, so it can
498    /// safely be used as an optimistic-concurrency precondition for source edits.
499    #[must_use]
500    pub const fn source_revision(&self) -> u64 {
501        self.source_revision
502    }
503
504    /// Compiles and accepts a complete source replacement at one exact base revision.
505    ///
506    /// # Errors
507    /// Rejects stale or invalid candidates without changing the accepted runtime,
508    /// journal, source revision, or playback state owned by the host.
509    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}