Skip to content

v0.4.3 — bound app.notify event history

Choose a tag to compare

@wmantly wmantly released this 16 Sep 01:30
· 8 commits to main since this release
6063bf4

Fixed

app.notify grew without bound and re-collapsed its whole history on every event.

The feed subscribes to every model event for the life of the tab and unshifted each one onto app.notify.events, which nothing ever trimmed — the existing cap was on rendered rows, not on retained events. The comment said so outright: "Cap on rendered rows. History keeps everything the server returns." That was written for a finite server response, not a live socket stream.

The worse half: push() calls render() on every event, and render() called collapse(this.events) over the entire array, allocating a fresh group object for every non-collapsing entry before discarding all but the first maxRows. So one arriving event cost O(history), on an array that only ever grew. With 100k events banked, each new event walks 100k entries and allocates ~100k objects.

Harmless on a quiet page. Not on one whose models churn on a timer: in theta-directory a Proxmox discovery poll rewrites last_seen on every discovered guest, and each write publishes model:Resource:update. A Directory tab left open accumulated events all day and was killed at 4GB.

Two changes:

  • events is capped at config.maxEvents, trimmed oldest-first on both the live path and the server feed.
  • collapse() takes a limit and stops once it has enough groups. Collapsing is strictly local — an event only ever merges into the group immediately before it — so rendered rows are byte-identical to a full walk.

The cap sits well above maxRows (30) on purpose, so collapse counts stay honest: the feed should say "203 resources updated", not "30". A burst longer than the cap undercounts at its tail, which is the right thing to lose. unread is unaffected — it still counts every event that arrived.

Added

  • app.notify config key maxEvents (default 1000): cap on events retained in memory, independent of maxRows.

Checks

96 tests pass, including three new ones: the cap holds under 5× overflow while keeping newest-first and preserving unread; the server feed is capped the same way as the live stream; and early-exit collapsing renders identical rows to a full walk (alternating models — the worst case for an off-by-one).

Measured over 20k events: events.length 20000 → 1000, push throughput 23.0s → 14.9s, with the gap widening as history grows.

Full Changelog: v0.4.2...v0.4.3