Skip to content

A filtered queue is a view of another queue, not a copy - #308

Merged
Sawtaytoes merged 1 commit into
mainfrom
filtered-queues
Sep 4, 2026
Merged

A filtered queue is a view of another queue, not a copy#308
Sawtaytoes merged 1 commit into
mainfrom
filtered-queues

Conversation

@Sawtaytoes

@Sawtaytoes Sawtaytoes commented Sep 4, 2026

Copy link
Copy Markdown
Owner

A Filtered queue shows part of another queue. Its whole record on disk is four keys:

- id: strips
  label: Strips
  filtered_from: reading
  filter:
    libraries: [ "5" ]

Why

I read one queue on two devices. One of them renders both of its libraries correctly. The
other does not, and the cause is a Kavita reader defect this repo already records in
providers/kavita.ts materialize()Kareadita/Kavita#4859.
The reader does not remount on auto-advance, so a chapter opened after one from a different
library keeps the previous one's scroll mode and width.

2026-08-17-the-reading-list-crosses-libraries-again decided to write the whole mixed
lineup and let the reader back out and reopen, because cutting the list at the first library
change left it a third of its proper size. A filtered queue is the answer that record did not
have: a second reading list that is format-homogeneous by construction, beside a parent
list that stays whole.

The four rules, and each one is about sharing

  1. No entries of its own. queues.entryOwner() resolves its id to the parent, so every
    read and every per-entry write lands on the parent's line. Add a series here, it appears
    there. Finish it there, it is finished here. Two entry lists would drift the first time
    either was added to.
  2. It inherits everything else. Provider, lanes, batch size, skip list, audience. The
    merge runs on the raw entries in server/src/filteredQueues.ts, before either
    normalizer
    sets.ts and engine/routing.ts parse this file twice for two readers,
    and a view that resolved in one and not the other would appear on the page and then refuse
    to launch.
  3. Its own runtime artifact. It creates and rebuilds its own Kavita reading list, titled
    from its own name, beside the parent's. This is the one thing it does not share, and it is
    the point.
  4. It may not set the order. A subset's reorder names only the keys it can see, and
    applyOrder sorts the rest to the tail — dragging two items in the view would sweep every
    item it cannot see to the bottom of the parent. Refused with a 409 naming the queue to
    open instead, and not offered as a drag in either the shelf or the grid.

An item whose library cannot be read is kept. Losing one of a hand-curated entry to a
metadata gap is worse than showing one that does not belong: the second is visible and
explains itself.

Screenshots

Fixture data throughout — e2e/fake-kavita.ts serves an invented library ("Volumes" and
"Strips", eight series that do not exist), so nothing real is in these images.

The Queues page — the view nested under the queue it views

Before After
before after

Before, filtered_from means nothing: "Strips" is an ordinary shelf with no provider and no
entries, sitting at the top level reading Empty. After, it is indented behind a rule under
"Comics & Strips", badged Filtered, linking its parent, and holding 5 of the parent's 8.

The view's own page

Before After
before after

After, the page opens by saying what it shows and where the order is set, and the lane hint
drops its "drag to reorder" clause because dragging is refused here.

Gates

  • e2e/filtered-queue-test.ts — 11 assertions, all four rules end to end: both parsers
    inherit identically, the provider block is narrowed, entries read and write on the parent,
    done is shared, the lineup is narrowed, the parent is unaffected, and the two reading lists
    are separate.
  • e2e/fake-kavita.ts — the offline Kavita the reading suites and the screenshots run
    against. New, and reusable by anything else that needs a reading fixture.
  • Unit tests for both filteredQueues.ts modules.
  • Typecheck, yarn test (web 332, server 386) and lint all clean.

Not built, deliberately

A filtered push queue (Plex). The read paths are provider-neutral, but a Plex queue also
records progress on the session / finished.ts path, which is keyed on the set id it was
launched under and does not go through queues.entryOwner(). It would record its watches
against itself, which breaks rule 1. Kavita has no such path — the read state lives in Kavita
and is the same state either way — so the reading case is complete and the watching case is
the follow-up.

Also not built: creating one from the editor, and filters other than libraries. The
filter mapping exists so the next one costs a key.

Decision record: docs/decisions/2026-09-04-a-filtered-queue-is-a-view-of-another-queue-not-a-copy.md

🤖 Generated with Claude Code

A Filtered queue shows part of another queue. Four keys on disk:

    - id: webtoons
      label: Webtoons
      filtered_from: manga_webtoons
      filter:
        libraries: [ "5" ]

…and four rules, every one of them about SHARING:

  * It holds NO entries of its own. `queues.entryOwner()` resolves its id to the
    parent, so an add from the view lands on the parent's line and a finish counts
    in both. Two entry lists would drift the first time either was added to, and
    two sets of done flags are two answers to "where am I".
  * It inherits every other field. `filteredQueues.ts` merges the parent underneath
    the raw entry BEFORE either normalizer — `sets.ts` and `engine/routing.ts` parse
    this file twice, and a view that resolved in one and not the other would appear
    on the page and then refuse to launch.
  * It builds its OWN Kavita reading list, titled from its own name, beside the
    parent's. This is the one thing it does not share, and it is the point: the
    reader does not remount on auto-advance (Kareadita/Kavita#4859), so a manga
    chapter after a webtoon keeps the webtoon's scroll mode. A view over one library
    is format-homogeneous by construction while the parent's list stays whole.
  * It may NOT set the order. A subset's key list names only what it can see, and
    `applyOrder` sorts the rest to the tail — reordering two webtoons would sweep
    every manga to the bottom of the PARENT. Refused with a 409 that names the queue
    to open instead, and not offered as a drag in either the shelf or the grid.

An item whose library cannot be read is KEPT: losing one of the owner's curated
entries to a metadata gap is worse than showing one that does not belong.

In the UI a view is nested under the queue it views — indented behind a rule, badged
`Filtered`, linking its parent — and its own page opens with a line saying what it
shows and where the order is set.

Gates: `e2e/filtered-queue-test.ts` pins all four rules end to end against a stubbed
Kavita, `e2e/fake-kavita.ts` + `e2e/shot-filtered-queue.ts` drive the UI offline on
invented fixtures, and both `filteredQueues.ts` modules carry unit tests.

NOT built, and deliberately: a filtered PUSH queue (Plex). Its progress is also
written on the session/`finished.ts` path, which is keyed on the launched set id and
would record a view's watches against itself.

Decision: docs/decisions/2026-09-04-a-filtered-queue-is-a-view-of-another-queue-not-a-copy.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Sawtaytoes
Sawtaytoes merged commit 9f3a4b5 into main Sep 4, 2026
2 checks passed
@Sawtaytoes
Sawtaytoes deleted the filtered-queues branch September 4, 2026 16:22
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.

1 participant