use std::collections::BTreeMap;
use brink_format::{SaveState, Value};
use serde::{Deserialize, Serialize};
use crate::error::RuntimeError;
use crate::rng::{FastRng, StoryRng};
use crate::story::Story;
use crate::story::{
ExternalFnHandler, ExternalResult, FallbackHandler, Line, StepOutcome, StoryStatus,
};
pub const SESSION_JOURNAL_VERSION: u32 = 1;
pub const SESSION_JOURNAL_CAP: usize = 65_536;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SessionJournal {
pub version: u32,
pub program_checksum: u32,
pub seed: Option<u64>,
pub events: Vec<JournalEvent>,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint: Option<SaveState>,
}
impl SessionJournal {
#[must_use]
pub fn new(program_checksum: u32, seed: Option<u64>) -> Self {
Self {
version: SESSION_JOURNAL_VERSION,
program_checksum,
seed,
events: Vec::new(),
truncated: false,
checkpoint: None,
}
}
fn push(&mut self, event: JournalEvent) {
if self.events.len() >= SESSION_JOURNAL_CAP {
self.truncated = true;
return;
}
self.events.push(event);
}
#[must_use]
pub fn len(&self) -> usize {
self.events.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct JournalEvent {
pub kind: EventKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub anchor: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flow: Option<String>,
}
impl JournalEvent {
#[must_use]
pub fn new(kind: EventKind) -> Self {
Self {
kind,
anchor: None,
flow: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EventKind {
Start {
#[serde(default, skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
args: Vec<Value>,
},
Choice {
index: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
label: Option<String>,
},
External {
name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
args: Vec<Value>,
result: Value,
},
SetVar { name: String, value: Value },
GoToPath {
path: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
args: Vec<Value>,
},
LoadState { state: SaveState },
Call {
name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
args: Vec<Value>,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ExternalReplayMode {
#[default]
Recorded,
Live,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ReplayOutcome {
Replayed { warnings: Vec<ReplayWarning> },
Diverged {
at_event: usize,
expected: Box<JournalEvent>,
found: DivergenceFound,
},
Failed { at_event: usize, reason: FailReason },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ReplayWarning {
ChoiceLabelDrift {
at_event: usize,
index: u32,
recorded: String,
found: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DivergenceFound {
ChoiceIndexOutOfRange { index: u32, available: usize },
NotWaitingForChoice,
UnknownPath { path: String },
UnexpectedEvent,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum FailReason {
RuntimeError { message: String },
Budget,
AwaitingExternal { name: String },
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StateSnapshot {
pub globals: BTreeMap<String, Value>,
pub lists: BTreeMap<String, SnapshotList>,
pub turn_index: u32,
pub visit_counts: BTreeMap<String, u32>,
pub turn_counts: BTreeMap<String, u32>,
pub call_stack: Vec<SnapshotFrame>,
pub status: SnapshotStatus,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SnapshotList {
pub items: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SnapshotFrame {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
pub temps: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SnapshotStatus {
Active,
WaitingForChoice,
Done,
Ended,
}
impl From<StoryStatus> for SnapshotStatus {
fn from(s: StoryStatus) -> Self {
match s {
StoryStatus::Active => Self::Active,
StoryStatus::WaitingForChoice => Self::WaitingForChoice,
StoryStatus::Done => Self::Done,
StoryStatus::Ended => Self::Ended,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct StateDiff {
pub added_globals: BTreeMap<String, Value>,
pub removed_globals: BTreeMap<String, Value>,
pub changed_globals: BTreeMap<String, (Value, Value)>,
pub list_deltas: BTreeMap<String, ListDelta>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_index: Option<(u32, u32)>,
pub pushed_frames: Vec<SnapshotFrame>,
pub popped_frames: Vec<SnapshotFrame>,
}
impl StateDiff {
#[must_use]
pub fn is_empty(&self) -> bool {
self.added_globals.is_empty()
&& self.removed_globals.is_empty()
&& self.changed_globals.is_empty()
&& self.list_deltas.is_empty()
&& self.turn_index.is_none()
&& self.pushed_frames.is_empty()
&& self.popped_frames.is_empty()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListDelta {
pub added: Vec<String>,
pub removed: Vec<String>,
}
#[must_use]
pub fn diff(a: &StateSnapshot, b: &StateSnapshot) -> StateDiff {
let mut d = StateDiff::default();
for (name, av) in &a.globals {
match b.globals.get(name) {
None => {
d.removed_globals.insert(name.clone(), av.clone());
}
Some(bv) if bv != av => {
d.changed_globals
.insert(name.clone(), (av.clone(), bv.clone()));
}
Some(_) => {}
}
}
for (name, bv) in &b.globals {
if !a.globals.contains_key(name) {
d.added_globals.insert(name.clone(), bv.clone());
}
}
let mut list_names: Vec<&String> = a.lists.keys().chain(b.lists.keys()).collect();
list_names.sort_unstable();
list_names.dedup();
for name in list_names {
let empty = SnapshotList { items: Vec::new() };
let al = a.lists.get(name).unwrap_or(&empty);
let bl = b.lists.get(name).unwrap_or(&empty);
if al == bl {
continue;
}
let added: Vec<String> = bl
.items
.iter()
.filter(|i| !al.items.contains(i))
.cloned()
.collect();
let removed: Vec<String> = al
.items
.iter()
.filter(|i| !bl.items.contains(i))
.cloned()
.collect();
if !added.is_empty() || !removed.is_empty() {
d.list_deltas
.insert(name.clone(), ListDelta { added, removed });
}
}
if a.turn_index != b.turn_index {
d.turn_index = Some((a.turn_index, b.turn_index));
}
let a_outer: Vec<&SnapshotFrame> = a.call_stack.iter().rev().collect();
let b_outer: Vec<&SnapshotFrame> = b.call_stack.iter().rev().collect();
let common = a_outer
.iter()
.zip(b_outer.iter())
.take_while(|(x, y)| x == y)
.count();
d.pushed_frames = b_outer[common..].iter().map(|f| (*f).clone()).collect();
d.popped_frames = a_outer[common..].iter().map(|f| (*f).clone()).collect();
d
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error(
"mutation `{op}` attempted mid-turn; set_var/go_to_path/load_state are turn-boundary only"
)]
MutationMidTurn { op: &'static str },
#[error("journal program checksum {journal} does not match program {program}")]
ChecksumMismatch { journal: u32, program: u32 },
#[error(transparent)]
Runtime(#[from] RuntimeError),
}
struct JournalingHandler<'a, H: ExternalFnHandler + ?Sized> {
inner: &'a H,
sink: std::cell::RefCell<&'a mut Vec<(String, Vec<Value>, Value)>>,
}
impl<'a, H: ExternalFnHandler + ?Sized> JournalingHandler<'a, H> {
fn new(inner: &'a H, sink: &'a mut Vec<(String, Vec<Value>, Value)>) -> Self {
Self {
inner,
sink: std::cell::RefCell::new(sink),
}
}
}
impl<H: ExternalFnHandler + ?Sized> ExternalFnHandler for JournalingHandler<'_, H> {
fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
let result = self.inner.call(name, args);
if let ExternalResult::Resolved(value) = &result {
self.sink
.borrow_mut()
.push((name.to_owned(), args.to_vec(), value.clone()));
}
result
}
}
struct RecordedReplayHandler<'a> {
queue: std::cell::RefCell<&'a mut std::collections::VecDeque<(String, Vec<Value>, Value)>>,
}
impl ExternalFnHandler for RecordedReplayHandler<'_> {
fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
let mut q = self.queue.borrow_mut();
match q.front() {
Some((n, a, _)) if n == name && a.as_slice() == args => {
let (_, _, result) = q.pop_front().unwrap_or_else(|| {
(String::new(), Vec::new(), Value::Null)
});
ExternalResult::Resolved(result)
}
_ => ExternalResult::Fallback,
}
}
}
pub struct StorySession<'p, R: StoryRng = FastRng> {
story: Story<'p, R>,
journal: SessionJournal,
started: bool,
pending_replay: Option<PendingReplay>,
}
struct PendingReplay {
remaining: std::collections::VecDeque<(usize, JournalEvent)>,
ext_queue: std::collections::VecDeque<(String, Vec<Value>, Value)>,
mode: ExternalReplayMode,
warnings: Vec<ReplayWarning>,
source_checkpoint: Option<SaveState>,
total_events: usize,
}
enum StepPark {
Awaiting { name: String },
Fail(FailReason),
}
impl<'p, R: StoryRng> StorySession<'p, R> {
#[must_use]
pub fn new(story: Story<'p, R>, seed: Option<u64>) -> Self {
let checksum = story.program().source_checksum();
Self {
journal: SessionJournal::new(checksum, seed),
story,
started: false,
pending_replay: None,
}
}
#[must_use]
pub fn journal(&self) -> &SessionJournal {
&self.journal
}
pub fn export_journal(&mut self) -> SessionJournal {
self.refresh_checkpoint();
let checksum = self.journal.program_checksum;
let seed = self.journal.seed;
std::mem::replace(&mut self.journal, SessionJournal::new(checksum, seed))
}
#[must_use]
pub fn story(&self) -> &Story<'p, R> {
&self.story
}
pub fn story_mut(&mut self) -> &mut Story<'p, R> {
&mut self.story
}
fn refresh_checkpoint(&mut self) {
self.journal.checkpoint = Some(self.story.save_state());
}
fn ensure_started(&mut self) {
if !self.started {
self.started = true;
self.journal.push(JournalEvent::new(EventKind::Start {
path: None,
args: Vec::new(),
}));
}
}
pub fn advance(&mut self) -> Result<StepOutcome, RuntimeError> {
self.advance_with(&FallbackHandler)
}
pub fn advance_with(
&mut self,
handler: &dyn ExternalFnHandler,
) -> Result<StepOutcome, RuntimeError> {
self.ensure_started();
let mut sink: Vec<(String, Vec<Value>, Value)> = Vec::new();
let outcome = {
let jh = JournalingHandler::new(handler, &mut sink);
self.story.advance_with(&jh)
};
for (name, args, result) in sink {
self.journal.push(JournalEvent::new(EventKind::External {
name,
args,
result,
}));
}
outcome
}
pub fn continue_single(&mut self) -> Result<Line, RuntimeError> {
self.ensure_started();
let mut sink: Vec<(String, Vec<Value>, Value)> = Vec::new();
let outcome = {
let jh = JournalingHandler::new(&FallbackHandler, &mut sink);
self.story.continue_single_with(&jh)
};
for (name, args, result) in sink {
self.journal.push(JournalEvent::new(EventKind::External {
name,
args,
result,
}));
}
outcome
}
pub fn continue_to_pause(&mut self) -> Result<Vec<Line>, RuntimeError> {
self.ensure_started();
let mut sink: Vec<(String, Vec<Value>, Value)> = Vec::new();
let outcome = {
let jh = JournalingHandler::new(&FallbackHandler, &mut sink);
self.story.continue_maximally_with(&jh)
};
for (name, args, result) in sink {
self.journal.push(JournalEvent::new(EventKind::External {
name,
args,
result,
}));
}
outcome
}
pub fn choose(&mut self, index: usize) -> Result<(), RuntimeError> {
self.ensure_started();
let label = self
.story
.pending_choices()
.into_iter()
.find(|c| c.index == index)
.map(|c| c.text);
self.story.choose(index)?;
#[expect(clippy::cast_possible_truncation, reason = "choice indices are small")]
self.journal.push(JournalEvent::new(EventKind::Choice {
index: index as u32,
label,
}));
Ok(())
}
pub fn resolve_external(&mut self, value: Value) {
let name = self
.story
.pending_external_name()
.map(str::to_owned)
.unwrap_or_default();
let args = self.story.pending_external_args().to_vec();
self.journal.push(JournalEvent::new(EventKind::External {
name,
args,
result: value.clone(),
}));
self.story.resolve_external(value);
}
#[must_use]
pub fn has_pending_external(&self) -> bool {
self.story.has_pending_external()
}
pub fn set_var(&mut self, name: &str, value: Value) -> Result<bool, SessionError> {
self.require_turn_boundary("set_var")?;
let applied = self.story.set_variable(name, value.clone());
if applied {
self.journal.push(JournalEvent::new(EventKind::SetVar {
name: name.to_owned(),
value,
}));
}
Ok(applied)
}
pub fn go_to_path(&mut self, path: &str, args: &[Value]) -> Result<(), SessionError> {
self.require_turn_boundary("go_to_path")?;
self.ensure_started();
if args.is_empty() {
self.story.choose_path_string(path)?;
} else {
self.story.choose_path_string_with_args(path, args)?;
}
self.journal.push(JournalEvent::new(EventKind::GoToPath {
path: path.to_owned(),
args: args.to_vec(),
}));
Ok(())
}
pub fn load_state(&mut self, state: &SaveState) -> Result<(), SessionError> {
self.require_turn_boundary("load_state")?;
self.story.load_state(state);
self.journal.push(JournalEvent::new(EventKind::LoadState {
state: state.clone(),
}));
Ok(())
}
#[must_use]
pub fn save_state(&self) -> SaveState {
self.story.save_state()
}
pub fn call_function(
&mut self,
name: &str,
args: &[Value],
handler: &dyn ExternalFnHandler,
) -> Result<Value, RuntimeError> {
let result = self.story.call_function(name, args, handler)?;
self.journal.push(JournalEvent::new(EventKind::Call {
name: name.to_owned(),
args: args.to_vec(),
}));
Ok(result)
}
fn require_turn_boundary(&self, op: &'static str) -> Result<(), SessionError> {
if self.started && self.story.status_is_active() {
return Err(SessionError::MutationMidTurn { op });
}
Ok(())
}
#[must_use]
pub fn snapshot(&self) -> StateSnapshot {
self.story.state_snapshot()
}
#[must_use]
pub fn diff(a: &StateSnapshot, b: &StateSnapshot) -> StateDiff {
diff(a, b)
}
}
impl<'p, R: StoryRng> StorySession<'p, R> {
pub fn restore(
story: Story<'p, R>,
journal: SessionJournal,
) -> Result<(Self, ReplayOutcome), SessionError> {
let program_checksum = story.program().source_checksum();
if program_checksum == journal.program_checksum {
if let Some(checkpoint) = journal.checkpoint.clone() {
let mut session = Self {
story,
journal,
started: true,
pending_replay: None,
};
session.story.load_state(&checkpoint);
return Ok((
session,
ReplayOutcome::Replayed {
warnings: Vec::new(),
},
));
}
} else if journal.checkpoint.is_none() {
return Err(SessionError::ChecksumMismatch {
journal: journal.program_checksum,
program: program_checksum,
});
}
Ok(Self::replay(
story,
&journal,
ExternalReplayMode::Recorded,
None,
))
}
#[must_use]
pub fn replay(
story: Story<'p, R>,
journal: &SessionJournal,
mode: ExternalReplayMode,
live_handler: Option<&dyn ExternalFnHandler>,
) -> (Self, ReplayOutcome) {
let mut session = Self {
story,
journal: SessionJournal::new(0, None),
started: false,
pending_replay: None,
};
session.journal =
SessionJournal::new(session.story.program().source_checksum(), journal.seed);
let ext_queue = journal
.events
.iter()
.filter_map(|ev| match &ev.kind {
EventKind::External { name, args, result } => {
Some((name.clone(), args.clone(), result.clone()))
}
_ => None,
})
.collect();
let state = PendingReplay {
remaining: journal.events.iter().cloned().enumerate().collect(),
ext_queue,
mode,
warnings: Vec::new(),
source_checkpoint: journal.checkpoint.clone(),
total_events: journal.events.len(),
};
let outcome = session.drive_replay(state, live_handler);
(session, outcome)
}
#[must_use]
pub fn has_pending_replay(&self) -> bool {
self.pending_replay.is_some()
}
pub fn continue_replay(
&mut self,
live_handler: Option<&dyn ExternalFnHandler>,
) -> ReplayOutcome {
if let Some(state) = self.pending_replay.take() {
return self.drive_replay(state, live_handler);
}
let handler = live_handler.unwrap_or(&FallbackHandler);
let mut steps = 0usize;
loop {
if steps >= Self::REPLAY_STEP_BUDGET {
self.journal.truncated = true;
return ReplayOutcome::Failed {
at_event: self.journal.len(),
reason: FailReason::Budget,
};
}
steps += 1;
match self.advance_with(handler) {
Ok(StepOutcome::Line(line)) if line.is_terminal() => {
return ReplayOutcome::Replayed {
warnings: Vec::new(),
};
}
Ok(StepOutcome::Line(_)) => {}
Ok(StepOutcome::AwaitingExternal) => {
let name = self
.story
.pending_external_name()
.map(str::to_owned)
.unwrap_or_default();
return ReplayOutcome::Failed {
at_event: self.journal.len(),
reason: FailReason::AwaitingExternal { name },
};
}
Err(e) => {
self.journal.truncated = true;
return ReplayOutcome::Failed {
at_event: self.journal.len(),
reason: runtime_fail(e),
};
}
}
}
}
const REPLAY_STEP_BUDGET: usize = 100_000;
fn drive_replay(
&mut self,
mut state: PendingReplay,
live_handler: Option<&dyn ExternalFnHandler>,
) -> ReplayOutcome {
loop {
let Some((at, _)) = state.remaining.front().cloned() else {
if self.started {
let at = state.total_events.saturating_sub(1);
if let Err(park) =
self.replay_step_to_pause(state.mode, live_handler, &mut state.ext_queue)
{
return self.park_or_fail(state, at, park);
}
}
self.journal.checkpoint.clone_from(&state.source_checkpoint);
return ReplayOutcome::Replayed {
warnings: state.warnings,
};
};
let next_is_choice = matches!(
state.remaining.front().map(|(_, ev)| &ev.kind),
Some(EventKind::Choice { .. })
);
if next_is_choice
&& let Err(park) =
self.replay_step_to_pause(state.mode, live_handler, &mut state.ext_queue)
{
return self.park_or_fail(state, at, park);
}
let Some((i, ev)) = state.remaining.pop_front() else {
continue;
};
match self.replay_apply(i, &ev, state.mode, &mut state.warnings) {
Ok(()) => {}
Err(outcome) => return outcome,
}
}
}
fn park_or_fail(&mut self, state: PendingReplay, at: usize, park: StepPark) -> ReplayOutcome {
match park {
StepPark::Awaiting { name } => {
self.pending_replay = Some(state);
ReplayOutcome::Failed {
at_event: at,
reason: FailReason::AwaitingExternal { name },
}
}
StepPark::Fail(reason) => {
self.journal.truncated = true;
ReplayOutcome::Failed {
at_event: at,
reason,
}
}
}
}
fn replay_apply(
&mut self,
i: usize,
ev: &JournalEvent,
mode: ExternalReplayMode,
warnings: &mut Vec<ReplayWarning>,
) -> Result<(), ReplayOutcome> {
match &ev.kind {
EventKind::Start { path, args } => {
if self.started {
return Err(self.diverge_at(i, ev, DivergenceFound::UnexpectedEvent));
}
self.started = true;
self.journal.push(JournalEvent::new(EventKind::Start {
path: path.clone(),
args: args.clone(),
}));
if let Some(p) = path {
let jump = if args.is_empty() {
self.story.choose_path_string(p)
} else {
self.story.choose_path_string_with_args(p, args)
};
if jump.is_err() {
return Err(self.diverge_at(
i,
ev,
DivergenceFound::UnknownPath { path: p.clone() },
));
}
}
}
EventKind::External { .. } => {
if mode == ExternalReplayMode::Recorded {
self.journal.push(ev.clone());
}
}
EventKind::Choice { index, label } => {
self.replay_choice(i, *index, label.as_ref(), ev, warnings)?;
}
EventKind::SetVar { name, value } => {
self.story.set_variable(name, value.clone());
self.journal.push(ev.clone());
}
EventKind::GoToPath { path, args } => {
let jump = if args.is_empty() {
self.story.choose_path_string(path)
} else {
self.story.choose_path_string_with_args(path, args)
};
if jump.is_err() {
return Err(self.diverge_at(
i,
ev,
DivergenceFound::UnknownPath { path: path.clone() },
));
}
self.journal.push(ev.clone());
}
EventKind::LoadState { state } => {
self.story.load_state(state);
self.journal.push(ev.clone());
}
EventKind::Call { name, args } => {
let _ = self.story.call_function(name, args, &FallbackHandler);
self.journal.push(ev.clone());
}
}
Ok(())
}
fn replay_choice(
&mut self,
at: usize,
index: u32,
label: Option<&String>,
ev: &JournalEvent,
warnings: &mut Vec<ReplayWarning>,
) -> Result<(), ReplayOutcome> {
if !self.story.status_is_waiting_for_choice() {
return Err(self.diverge_at(at, ev, DivergenceFound::NotWaitingForChoice));
}
let presented = self.story.pending_choices();
let available = presented.len();
let Some(current) = presented.iter().find(|c| c.index == index as usize) else {
return Err(self.diverge_at(
at,
ev,
DivergenceFound::ChoiceIndexOutOfRange { index, available },
));
};
if let Some(recorded) = label
&& recorded != ¤t.text
{
warnings.push(ReplayWarning::ChoiceLabelDrift {
at_event: at,
index,
recorded: recorded.clone(),
found: current.text.clone(),
});
}
if let Err(e) = self.story.choose(index as usize) {
self.journal.truncated = true;
return Err(ReplayOutcome::Failed {
at_event: at,
reason: runtime_fail(e),
});
}
self.journal.push(ev.clone());
Ok(())
}
fn replay_step_to_pause(
&mut self,
mode: ExternalReplayMode,
live_handler: Option<&dyn ExternalFnHandler>,
ext_queue: &mut std::collections::VecDeque<(String, Vec<Value>, Value)>,
) -> Result<(), StepPark> {
let mut steps = 0usize;
loop {
if steps >= Self::REPLAY_STEP_BUDGET {
return Err(StepPark::Fail(FailReason::Budget));
}
steps += 1;
let outcome = match mode {
ExternalReplayMode::Recorded => {
let h = RecordedReplayHandler {
queue: std::cell::RefCell::new(ext_queue),
};
self.story.advance_with(&h)
}
ExternalReplayMode::Live => {
let mut sink: Vec<(String, Vec<Value>, Value)> = Vec::new();
let outcome = {
let h = live_handler.unwrap_or(&FallbackHandler);
let jh = JournalingHandler::new(h, &mut sink);
self.story.advance_with(&jh)
};
for (name, args, result) in sink {
self.journal.push(JournalEvent::new(EventKind::External {
name,
args,
result,
}));
}
outcome
}
};
match outcome {
Ok(StepOutcome::Line(line)) => {
if line.is_terminal() {
return Ok(());
}
}
Ok(StepOutcome::AwaitingExternal) => {
let name = self
.story
.pending_external_name()
.map(str::to_owned)
.unwrap_or_default();
return Err(StepPark::Awaiting { name });
}
Err(e) => return Err(StepPark::Fail(runtime_fail(e))),
}
}
}
fn diverge_at(
&mut self,
at: usize,
expected: &JournalEvent,
found: DivergenceFound,
) -> ReplayOutcome {
self.journal.truncated = true;
self.journal.checkpoint = None;
ReplayOutcome::Diverged {
at_event: at,
expected: Box::new(expected.clone()),
found,
}
}
}
fn runtime_fail(e: RuntimeError) -> FailReason {
match e {
RuntimeError::StepLimitExceeded(_) | RuntimeError::LineLimitExceeded(_) => {
FailReason::Budget
}
other => FailReason::RuntimeError {
message: other.to_string(),
},
}
}