A filtered queue is a view of another queue, not a copy - #308
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Filtered queue shows part of another queue. Its whole record on disk is four keys:
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-againdecided to write the whole mixedlineup 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
queues.entryOwner()resolves its id to the parent, so everyread 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.
merge runs on the raw entries in
server/src/filteredQueues.ts, before eithernormalizer —
sets.tsandengine/routing.tsparse 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.
from its own name, beside the parent's. This is the one thing it does not share, and it is
the point.
applyOrdersorts the rest to the tail — dragging two items in the view would sweep everyitem 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.tsserves 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,
filtered_frommeans nothing: "Strips" is an ordinary shelf with no provider and noentries, 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
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 parsersinherit 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 runagainst. New, and reusable by anything else that needs a reading fixture.
filteredQueues.tsmodules.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.tspath, which is keyed on the set id it waslaunched under and does not go through
queues.entryOwner(). It would record its watchesagainst 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. Thefiltermapping 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