AOF with fs.createWriteStream

Before the code: what crash-safety actually means

Your store is fast because it lives in memory. The price of that speed is that a single power cut wipes everything. This module fixes that. The trick is simple: keep a flight recorder. Every write the server accepts is appended to a file on disk. After a crash, you replay the file from the top and the server is back. That is the entire append-only log idea, used inside Redis, every SQL database, Kafka, and even git.

Write to memory, log to disk

Every write goes two places at once: the in-memory Map for fast reads, and the log file so the Map can be rebuilt after a crash.

Quiz: Quiz

Loading practice…

Open the file once with createWriteStream({ flags: 'a' }) and keep the handle. Every write goes through OS buffering with minimal syscalls.

05-aof-persistence/server.mjs
javascript
function aofAppend(args) {
  if (!aofStream) aofStream = createWriteStream(AOF_PATH, { flags: "a" });
  aofStream.write(encodeArray(args));
}

aofAppend writes via the stream. No open + close per call. encodeArray is the same RESP array encoder from the store lessons; the step file names it encodeRESPArray, but it is one helper, so we keep one name here.

One refactor before replay makes sense: dispatch now goes through a wrapper. handleCommand(frame, persist) looks up the handler in the COMMANDS object, runs it, and, when persist is true and the command is a successful write, calls aofAppend. Replay passes persist as false. Without that flag, replaying the log would append every command back onto the log it came from, doubling the file on every restart.

05-aof-persistence/server.mjs
javascript
function aofReplay() {
  if (!existsSync(AOF_PATH)) return { replayed: 0, dropped: 0 };
  const buf = readFileSync(AOF_PATH);
  let cursor = 0, replayed = 0, dropped = 0;
  while (cursor < buf.length) {
    let r;
    try { r = parseResp(buf, cursor); }
    catch (e) { dropped = buf.length - cursor; break; }
    if (r === null) { dropped = buf.length - cursor; break; }
    if (Array.isArray(r.value)) {
      handleCommand(r.value, false);
      replayed++;
    }
    cursor += r.consumed;
  }
  return { replayed, dropped };
}

Replay uses the same parseResp. Partial trailing data is detected and skipped.