Skip to content

v0.53.1

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 02 Sep 01:45
· 140 commits to master since this release

v0.53.1, 2026-09-02

Changed

  • An i.redd.it expando now loads the original image instead of reddit's signed
    preview. The preview is re-encoded to webp, so an expando showed a lossy copy
    at whatever size reddit felt like serving, and saving it wrote webp bytes into
    a file named .png because the signed URL's path still ends in .png. Images
    past 24 megapixels keep showing the preview, which is what was capping the
    bytes an expando pulls inline, but the save control is pointed at the original
    either way. A crosspost reads all of this from the parent post, the same one
    its preview already came from.
  • A filter rule's regular expression is compiled once when the rules are parsed
    rather than once per rule per post. The catastrophic-pattern check and
    new RegExp ran inside the per-post evaluation, and the shipped default rule
    set contains an enabled regex rule, so that was everyone's default path: about
    two thousand compilations across a thousand-post scroll for a pattern that
    never changed. A malformed or catastrophic pattern is still refused, now at
    parse time, and still matches nothing rather than throwing mid-filter.
  • A live score tick on current Reddit no longer re-runs a post's whole
    preparation. Reddit updates the score as an attribute, and the observer routed
    that through the full pass — eight to ten attribute copies, five class toggles,
    up to nine selector queries and the shadow-part exposure, per post, per tick,
    hundreds of times while a feed hydrates. The attribute path now does only the
    mirroring it needs. A post that has never been prepared still gets the full
    pass even if an attribute change is the first thing to reach it.

Added

  • yarn publish:release, the half the release helper deliberately stopped
    before. It runs every gate and the Firefox audit, refuses a dirty tree, the
    wrong remote, a lightweight tag, a tag that does not name the commit being
    published, a tagged tree whose version or CHANGELOG does not match, or a
    missing artifact. Then it pushes, hashes both ZIPs, renames them after the
    tag, uploads them with a SHA256SUMS file, and reads the release back off
    GitHub to confirm the assets arrived at the sizes it built. Re-running an
    already-published version completes it rather than making a second one.

Fixed

  • Release files now stay in a system temporary directory while Git's push hooks
    repeat the build. The hooks clear dist, which previously removed the staged
    ZIPs, checksum file, and release notes after the tag reached GitHub but before
    the release could be created.

  • The thread minimap forced a layout per comment, every time it rebuilt. It read
    each comment's rectangle and appended that comment's stripe in the same loop,
    so every read had to flush the previous write — and the whole rail is rebuilt
    200ms after any mutation anywhere in the comment area, which includes every
    class this extension toggles on any comment. Rectangles are now read in one
    pass, the stripes are built off-document and attached once, a single delegated
    listener on the rail replaces one per stripe, and the scroll handler is
    frame-throttled against a cached document height instead of measuring one on
    every event.

  • Six defects in the route-aware lifecycle, found by an adversarial review of
    the change that introduced it. A route change between DOMContentLoaded and
    load ran afterLoad for every eligible module and then load ran them all
    again — measured as two IntersectionObservers and a second scroll listener from
    the media expander alone. The stage gate did not cover beforeLoad, which is
    the only stage two comments-scoped modules have, so on the delayed-page-type
    case the change was named for they still ran zero times rather than once. The
    route scope was aborted on every navigation with nothing subscribed to it,
    because the stages it started were handed the page-length signal instead. The
    four stages a route change offers were dispatched together rather than in
    order. And the full per-post preparation still ran on every child mutation,
    which reddit produces throughout hydration — it now runs when slotted content
    actually arrives.

  • Old Reddit's endless scroll could stall on a page of pure overlap. Skipping
    every repeated row means such a page changes the DOM by nothing at all, and the
    only thing that asks for another page is an IntersectionObserver, which fires
    on transitions — so with the sentinel already in view and the geometry
    unchanged, no further callback ever arrived. A page that adds nothing now
    fetches the next one itself, bounded at three in a row so a listing that has
    genuinely run out is not walked to reddit's pagination ceiling.

  • Current Reddit navigates without ever unloading, and the module lifecycle did
    not know it. A pushState that changed no DOM was invisible — which is the
    normal case, because reddit swaps the URL before rendering the new view — so
    only the back button and an incidental mutation were ever noticed. The
    Navigation API's navigatesuccess is now the primary signal, with the observer
    and popstate behind it, and every source runs through one dedupe keyed on the
    URL plus what reddit says the page is.

    Two consequences follow. A page stage now runs for modules that have only just
    become eligible, so a module whose include finally matches gets its turn
    instead of sitting the session out; modules that already ran are not run again,
    which would have duplicated every observer and control they made. And there is
    a route-length scope beside the page-length one, so work started for the
    listing you left can be cancelled instead of running against a page that no
    longer exists.

    Page-scoped stages also wait for reddit's own answer about what the page is
    before deciding who belongs on it. /r/x/s/<id> share links are post pages
    that match no path pattern, so the fallback called them listings and every
    comments-scoped module sat them out permanently.

  • Hiding a post could destroy an embed instead of quietening it. Routing the
    hide path through the expando collapse lifecycle inherited that lifecycle's
    fallback: four of the thirty-five iframe hosts declare a pause command, and for
    the other thirty-one — CodePen, JSFiddle, Pastebin, the poll hosts, Twitch —
    collapse removes the frame. That is right when the reader asked for it and
    re-expanding rebuilds; it is wrong for a filter, which takes an in-progress
    edit or a filled-in poll with it and gives nothing back. Hiding now asks for a
    collapse that keeps content, so a frame with no pause command is left exactly
    where it was.

  • A back/forward-cache restore left old Reddit without its per-post work. The
    page signal was aborted on any pagehide, including one that only puts the
    page into the cache — and the document comes back with that signal still
    aborted, so the two waits that register a post's visible tasks failed before
    observing anything. Nothing appended after a restore got an expando, a filter,
    or a vote colour. Only a real teardown aborts now, and both waits swallow their
    own cancellation rather than raising an unhandled rejection per pending post on
    every navigation away.

  • Two contrast failures on the light Classic palette, found by putting current
    Reddit's injected controls under an automated WCAG sweep for the first time.
    Absolute timestamps were faded with opacity: 0.75, which took reddit's own
    tagline grey down to 3.16:1; they now use the managed muted ink, which is
    contrast-checked and flips with the palette where an opacity multiplier cannot.
    And the palette's muted grey was reddit's #666, which clears AA on white but
    not on the surfaces this theme tints with the accent — the submitter chip
    measured 4.09:1. It is a little darker now and clears both.

  • Three DOM waits could never end, and a multi-event wait left its losing
    listener attached. waitForSelectorMatch was the expensive one: its only
    caller runs for every link post, waiting for an expando the reader may never
    open, so an attribute observer and a pending async frame accumulated per post
    across an infinite scroll. All three now take the same timeout and abort
    contract the other waiters use, and core hands them a signal tied to the page's
    lifetime. waitForEvent runs one race and removes every listener it installed
    when any of them wins.

  • A shred run that lost its account kept deleting. The heartbeat renewed the
    lease and threw the answer away, so a run whose lease had gone — to a service
    worker restart, or to timer throttling in a background tab outrunning the
    expiry — carried on issuing edits and deletes while another tab held the
    account. That is the exact overlap the lease exists to prevent. A heartbeat
    that cannot hold the account now re-asserts once, and stops the run between
    comments if the account really has been taken, saying so in the outcome.

  • Typing in a shred panel's confirmation box while it was asking the background
    whether another run was going re-enabled the Shred button. The second click's
    refusal landed after the first run had started and overwrote its live progress
    line with "another run is already going", then handed the button back mid-run.

  • Hiding a post left an expando's embedded player running. Pausing a <video>
    reaches the media the page owns, and nothing else: an expando this extension
    opened holds its own player, which knows the pause command its host
    understands. Hiding now collapses those through the same lifecycle a manual
    collapse uses, so unhiding rebuilds them. A third-party frame nobody declared a
    pause command for is deliberately left alone — the only generic way to stop one
    is to drop or reload it, and a filter hides a post you may unhide a moment
    later.

  • Old Reddit's endless scroll could show the same post twice, and drew a "page
    loaded" separator over nothing when a whole page was overlap. Reddit's after
    cursor overlaps by design, and a post that moves up the listing between two
    fetches is served twice. The repeat was removed after being fetched, parsed,
    inserted and registered, which is the wrong layer to rely on alone. The append
    loop now compares against the fullnames already on the page and skips what it
    has, keeps rows that carry no usable id rather than guessing, and emits a
    separator only for a page that actually added something.

  • The optional-permission prompt could not work on Firefox. It opened in an
    extension popup window, and Firefox refuses permissions.request() from one
    (Mozilla Bug 1957822), so the window appeared and then could not do the single
    thing it existed for. Firefox now gets a normal tab, which is a context the
    browser is willing to raise its own permission panel from; Chrome keeps the
    centred popup window, which works there.

  • Two tabs could shred the same account at once. The guard was a boolean scoped
    to one tab, which by construction could never see the tab that matters, and
    the module's own header already described what happens when two runs overlap:
    they share reddit's per-account write limiter, and the second tries to delete
    comments the first already deleted, turning a normal outcome into a page of
    failures reported as untouched. Nothing on the page could arbitrate it either,
    because old Reddit and current Reddit are different origins.

    A run now takes a lease on the account, held by the extension background,
    which is the only context both renderers share. It is keyed by the normalized
    username, renewed while the run goes, released when the run ends, and expires
    on its own if the tab that held it dies. A second tab is refused, told where
    the other run is and how long it has been going, and can retry when that run
    finishes.