The Phantom Unread Badge — JavaScript Bug Hunt

Inspired by the eternally-wrong unread badges of chat apps. The socket re-delivers messages after reconnects, and marking a channel read somehow leaves…

  • Language: JavaScript
  • Layer: Frontend
  • Difficulty: Medium
  • Concepts: State, Dedupe
  • Modelled on: Slack
  • Visible tests: re-delivered messages count once; read clears everything up to and including upToId; messages after the read marker stay unread
  • Reward: 50 XP for a complete fix

Briefing

Inspired by the eternally-wrong unread badges of chat apps. The socket re-delivers messages after reconnects, and marking a channel read somehow leaves exactly one message behind.

unread.js folds the event stream into a badge count — two bugs, one function.

Bug report

BUG-1337 · Reported by: mobile team

Event stream contract:

  • { type: "message", id, channel } — ids are unique but MAY be re-delivered
  • { type: "read", channel, upToId } — clears every message with id <= upToId

Observed: badge shows 5 after a reconnect that re-sent 2 messages, and after "mark as read" one unread always remains.

Logs

[badge] general: events=6 unique=4 badge=6
[badge] after read upTo=42 -> badge=1 (expected 0)

The code as shipped

src/chat/unread.js (editable)

// Folds the socket event stream into unread counts per channel.
exports.unreadCounts = function (events) {
  var perChannel = {};

  for (var i = 0; i < events.length; i++) {
    var e = events[i];
    if (e.type === "message") {
      if (!perChannel[e.channel]) perChannel[e.channel] = [];
      perChannel[e.channel].push(e.id);
    } else if (e.type === "read") {
      var list = perChannel[e.channel] || [];
      perChannel[e.channel] = list.filter(function (id) {
        return id > e.upToId + 1;
      });
    }
  }

  var out = {};
  for (var ch in perChannel) out[ch] = perChannel[ch].length;
  return out;
};

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.