Skip to content

Channel history: keep recent lines on the device and show them at once - #220

Draft
rdrake wants to merge 5 commits into
evilnet:developfrom
rdrake:claude/project-thread-qgpumy
Draft

rdrake wants to merge 5 commits into
evilnet:developfrom
rdrake:claude/project-thread-qgpumy

Conversation

@rdrake

@rdrake rdrake commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator

Closes #219 (the display-cache version proposed there).

What

After a page load (a reload, an Android PWA brought back after it was discarded, an app launch), each channel shows its last scrollback straight away, even with no connection, instead of staying empty until CHATHISTORY answers.

  • client/js/irc/historycache.ts keeps each channel's newest 200 messages, actions and notices (only those with a msgid, at most 7 days old) in IndexedDB (seance-history, one record per channel). IrcClient.restoreChannelCache shows them as a history page as soon as the page loads.
  • A display cache, never evidence. Restored rows are marked cached (Channel.cachedIds, MsgRef.cached). Nothing that decides what the server still has to send reads them: not the catch-up walk's known set or floor, newestServerRef, TARGETS, or the ATTACH cursor. The connect-time fill runs exactly as before.
  • The JOIN's first LATEST page settles them (history.ts reconcileCached):
    • If the page shares a msgid with them and holds nothing unknown inside their time span, they are confirmed. With nothing new they stay where they are; otherwise they are delivered again with the new lines as one page above our JOIN.
    • Otherwise a new msg:drop {chan, ids} bus event takes them out, and the page shows as it would have without them.
    • A catch-up walk that stops short drops what the device kept from before the gap.
  • Writes and redactions. Each write merges with the stored record in a single transaction, so two tabs don't overwrite each other. A redaction leaves a content-free tombstone that every merge honours.
  • REDACT deletes from the device by msgid, whether or not the line is still loaded. This also fixes an existing bug: QueryLog kept a redacted private line that had already been trimmed from memory, and it came back on the next load.
  • Setting. Settings → General → Channel history (keepChannelHistory, on by default) turns it off and deletes everything stored. Removing a network or parting a channel forgets it. Queries stay in querylog.ts, whose reaction logic is now shared.

Why not more

A fuller design, which would use the store to skip server catch-up, read older pages from disk, fill holes with BETWEEN and fold in QueryLog/ClosedQueries, was reviewed adversarially and set aside (see #219). The current walk can't prove that stored lines are contiguous with the server's history. ATTACH replay is capped per channel and goes around the walk. Proving coverage is a separate, larger feature that probably needs server help.

Testing

  • test/irc/historycache.ts, 14 tests:
    • merge and tombstones across tabs
    • a redaction before the store is read
    • the confirm, reorder, drop and inside-span cases
    • cached rows never counting as server evidence
  • Full mocha suite: 5144 passing. pnpm lint, pnpm typecheck and i18n:check are clean.
  • tools/scenarios/history-cache.mjs, run against nefarious2 ircv3.2-upgrade with the dev config (all 7 checks pass). The page dials through lib/irc-proxy.mjs, so the socket can be refused across a reload. It covers:
    • restore with no connection
    • lines missed while away joining up in order
    • a gap larger than the first page, which drops the cached rows
    • the setting switched off, which empties IndexedDB

Open questions

  • On by default, or opt-in at first?
  • nefarious2 advertises evilnet/CHATHISTORYRETENTION, which could replace the fixed 7-day cap.
  • When there are new lines, the cached rows are delivered again as one page. This looks right on desktop; the scroll position on a phone hasn't been checked yet.

…ags, +reply (#5)

- CAP: a LIST reply spread over lines keeps every line, and an ACK spread over lines changes nothing until its last line.
- BATCH: the type is an opaque identifier, matched exactly.
- PRIVMSG/NOTICE: client tags (reply, edit) only go out once message-tags is enabled.
- Replies and reactions carry the ratified +reply alongside +draft/reply; +reply is read first. The service worker's notification reply does the same.
- ISUPPORT: a run of \xHH escapes is decoded as UTF-8, falling back to one byte per character.
# Conflicts:
#	client/js/irc/handlers/privmsg.ts
#	docs/resources/bus-contract.md
A page load (a reload, an Android PWA brought back after a discard, an
app launch) starts every channel empty until the server's history
answers. Keep each channel's newest 200 messages (msgid-bearing, 7 days
at most) in IndexedDB and show them as a history page as soon as the
page loads.

A display cache, never evidence: the restored rows are marked cached
(Channel.cachedIds, MsgRef.cached) and nothing that decides what the
server still has to send reads them -- the catch-up walk's known set and
floor, newestServerRef, TARGETS, the ATTACH cursor -- so the
connect-time fill runs exactly as before. The JOIN's first LATEST page
settles them (history.ts reconcileCached): when it shares a msgid with
them and holds nothing unknown inside their span they are confirmed
(left in place when nothing is new, else delivered again with the new
lines as one page above our JOIN); otherwise a new msg:drop event takes
them out of the buffer and the page shows as it would have. A catch-up
walk that stops short drops what the device kept from before the gap.

Each write merges with the stored record in one transaction, so two tabs
do not overwrite each other, and a redaction leaves a content-free
tombstone that every merge honours. REDACT now deletes the line from the
device by msgid whether or not it is still loaded -- which also fixes
QueryLog keeping a redacted private line that had been trimmed from
memory. Settings -> General -> Channel history turns it off and deletes
everything; removing a network or parting a channel forgets it. Queries
stay in querylog.ts, whose reaction logic is now shared.

Tests: test/irc/historycache.ts (merge, tombstones across tabs,
redaction before load, the confirm / reorder / drop / inside-span cases,
cached rows never counting as server evidence). Browser:
tools/scenarios/history-cache.mjs against a local nefarious2 -- restore
with the socket refused, the joined-up case, a gap larger than the first
page, the switch.

This branch was previously deployed

1 inactive deployment
github-pages — dd22caa0 Deployed Nov 8, 2026 by rdrake via deploy #763
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep recent channel history on the device (display cache)

1 participant