///|
pub fn Session::save(self : Session) -> String raise StoryError {
  // Check the exact encoded size before joining the full journal. A valid script
  // may contain very long choice identifiers reused by thousands of actions.
  let mut encoded_length = Json::object(
      Map([
        ("format", Json::string("moonstory-save/1")),
        ("source", Json::string(self.story.source)),
        ("actions", Json::array([])),
      ]),
    )
    .stringify()
    .length()
  for i = 0; i < self.actions.length(); i = i + 1 {
    encoded_length = encoded_length +
      Json::string(self.actions[i]).stringify().length() +
      (if i > 0 { 1 } else { 0 })
    if encoded_length > 2097152 {
      raise Limit("save exceeds 2097152 code units")
    }
  }
  let result = Json::object(
    Map([
      ("format", Json::string("moonstory-save/1")),
      ("source", Json::string(self.story.source)),
      ("actions", Json::array(self.actions.map(Json::string))),
    ]),
  ).stringify()
  if result.length() > 2097152 {
    raise Limit("save exceeds 2097152 code units")
  }
  result
}

///|
/// Validate and replay a save against the exact expected story. Historical emit
/// events are suppressed; the next transition resumes their sequence numbers.
pub fn restore(story : Story, saved : String) -> Session raise StoryError {
  if saved.length() > 2097152 {
    raise Limit("save exceeds 2097152 code units")
  }
  let value = @json.parse(saved) catch { _ => raise Save("invalid save JSON") }
  guard value is Object(o) else { raise Save("save must be an object") }
  if o.length() != 3 ||
    o.get("format") != Some(Json::string("moonstory-save/1")) {
    raise Save("unsupported save format or fields")
  }
  if o.get("source") != Some(Json::string(story.source)) {
    raise Save("save belongs to different story source")
  }
  guard o.get("actions") is Some(Array(actions)) else {
    raise Save("actions must be an array")
  }
  if actions.length() > 10000 {
    raise Limit("save exceeds 10000 player actions")
  }
  let mut state = start(story)
  for action in actions {
    guard action is String(id) else { raise Save("actions must be strings") }
    if !id.is_empty() && !identifier(id) {
      raise Save("invalid choice id in save")
    }
    state = if id.is_empty() { state.advance() } else { state.choose(id) }
  }
  {
    story: state.story,
    vars: state.vars,
    pc: state.pc,
    current: state.current,
    delivered: [],
    sequence: state.sequence,
    actions: state.actions,
  }
}