← Memento lesson
Lab

Memento

type Preferences = { branch: string; format: string; alerts: string[] };

// The memento: an empty token. Only the record that made it can read it.
type Checkpoint = Readonly<object>;

const copy = (p: Preferences): Preferences => ({ ...p, alerts: [...p.alerts] });

// The originator: it owns the preferences and decides what a checkpoint holds.
class MemberRecord {
  prefs: Preferences = {
    branch: 'East Branch',
    format: 'print',
    alerts: ['history', 'maps'],
  };
  loans = 2; // live: never captured, never restored
  private saved = new WeakMap<Checkpoint, Preferences>();

  capture(): Checkpoint {
    const token: Checkpoint = Object.freeze({});
    this.saved.set(token, copy(this.prefs));
    return token;
  }

  restore(token: Checkpoint): void {
    const saved = this.saved.get(token);
    if (!saved) throw new Error('foreign checkpoint');
    this.prefs = copy(saved);
  }

  edit(branch: string, format: string): void {
    this.prefs.branch = branch;
    this.prefs.format = format;
  }

  follow(subject: string): void {
    this.prefs.alerts.push(subject);
  }

  unfollow(subject: string): void {
    const at = this.prefs.alerts.indexOf(subject);
    if (at >= 0) this.prefs.alerts.splice(at, 1);
  }
}

// The caretaker: it keeps labelled tokens and never looks inside one.
class CheckpointHistory {
  private entries: { label: string; token: Checkpoint }[] = [];
  private member: MemberRecord;

  constructor(member: MemberRecord) {
    this.member = member;
  }

  save(label: string): void {
    this.entries.push({ label, token: this.member.capture() });
  }

  restore(label: string): void {
    const entry = this.entries.find((e) => e.label === label);
    if (!entry) throw new Error('no checkpoint');
    this.member.restore(entry.token);
  }
}

function summary(member: MemberRecord): string {
  const { branch, format, alerts } = member.prefs;
  const subjects = JSON.stringify(alerts);
  return `${branch}, ${format}, ${subjects}, loans ${member.loans}`;
}

const member = new MemberRecord();
const checkpoints = new CheckpointHistory(member);
checkpoints.save('My usual settings');

// The first experiment, then back. A book is checked out meanwhile.
member.edit('Riverside Branch', 'digital');
member.unfollow('history');
member.loans = 3;
checkpoints.restore('My usual settings');
const first = summary(member);

// The second experiment, then back to the same checkpoint.
member.edit('Harbor Branch', 'large print');
member.unfollow('history');
member.follow('poetry');
checkpoints.restore('My usual settings');
const second = summary(member);

console.log(`first:  ${first}`);
console.log(`second: ${second}`);