Undoable DatabaseLoading saved progress…

Undoable Database

Build an in-memory user database — a table of records, each keyed by an id — that supports the usual CRUD writes plus undo and redo across the history of those writes. Every mutation is one step: a single undo() reverts the last set or delete, and a single redo() re-applies the last thing you undid. Think of the editing history in a text editor, but the document is a key→record map. The one rule that trips people up: once you undo and then make a new write, the things you'd undone are gone — you can't redo into a future you've abandoned.

Signature

class UndoableDatabase {
  constructor();

  set(id, record): void;        // create or overwrite the record at `id`.
  get(id): record | undefined;  // read the record at `id`, or undefined if absent.
  delete(id): void;             // remove the record at `id`.
  undo(): void;                 // revert the single most recent mutation.
  redo(): void;                 // re-apply the single most recent undone mutation.
}

set and delete are the only mutations. Each one is a discrete, individually undoable step — there is no batching or grouping of several writes into one undo unit here.

Examples

// set / undo / redo over a single key.
const db = new UndoableDatabase();
db.set('u1', { name: 'Ada' });
db.get('u1');     // → { name: 'Ada' }
db.undo();
db.get('u1');     // → undefined   (the set is reverted)
db.redo();
db.get('u1');     // → { name: 'Ada' }   (the set is re-applied)
// A new write after an undo throws away the redo branch.
const db = new UndoableDatabase();
db.set('u1', { name: 'Ada' });
db.set('u2', { name: 'Bob' });
db.undo();                 // u2 is now redoable
db.set('u3', { name: 'Cy' }); // a fresh write — the u2 future is abandoned
db.redo();                 // no-op: there is nothing to redo
db.get('u2');              // → undefined   (u2 never comes back)
db.get('u3');              // → { name: 'Cy' }

Notes

  • Every set and delete is its own undo step. Overwriting an existing key counts too: a set('u1', …) on a key that already has a value is undoable back to the old value, not to "absent."
  • A new mutation after an undo clears the redo history. You can step back with undo, but the moment you set or delete again, anything you'd undone is discarded — redo then has nothing to do.
  • undo and redo at the boundaries are safe no-ops. Calling undo with no history (or past the start), or redo with nothing undone (or past the end), must not throw — it simply does nothing.
  • Returned records are isolated from internal state. A caller who mutates the object returned by get — including nested arrays or objects inside it — must not be able to corrupt the stored record or any point in its history.
  • Deleting a missing key is harmless. delete on an id that isn't present does not throw.
  • Don't worry about batching multiple writes into one undo unit, querying by anything other than id, capping the history size, or persisting across sessions — those are out of scope.
Loading editor…