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.
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.
// 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' }
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."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.get — including nested arrays or objects inside it — must not be able to corrupt the stored record or any point in its history.delete on an id that isn't present does not throw.id, capping the history size, or persisting across sessions — those are out of scope.