My attempt to get this working basically extended the existing system which used a count and some Booleans. After countless hours of trying to get this sync right, I came to a realization:
Trying to keep state by passing Booleans back and forth is like flinging paint at a wall and expecting the Mona Lisa to appear.
I had seenLatest, I had hasChanges and even resorted to forceUpdate. There was always a corner-case or timing/sequencing condition that would trip it up. And then I realized that I needed to embrace timing. Each syncable thing has a lastUpdated time and each client has a lastSaw time.
Key for getting this working was remembering to think about this from a user's point of view - they may well be logged in on three devices (aka clients) but once they have seen a notification on any device they don't want to see it again.
The pseudocode for this came down to:
Polling Loop / When a thing changes
userLastSawTimestamp = max(clientLastSawTimestamps) if (thing.lastUpdated > userLastSawTimestamp) { showNotification(thing) }
On User Viewing Thing (i.e. clearing the badge)
clientLastSawTimestamp[clientId] = now
And now it works how it should.
And I see failed attempts to do it correctly everywhere I look ... :-)