fix(r3): judge each rel once per request — a side or review item that vanishes mid-request never 500s
booth-dev's race note after the merge: the compare route resolved each side in _compare_side and again in _compare_ring, then ring.index(a) raised if the file vanished (or was relinked outside the booth) between the two; the review did the same through cring.index(f). The compare ring is now built once and the sides are judged by membership of it. The review re-judges its item and scans forward for the next comparable one (usually one step, no longer a resolve of the whole ring per render); an item no longer comparable renders the review without a Compare control, and C does nothing. The contract records the once-per-request rule and that the phone-width wrap covers doc.html's bar too. r3.toml: 59 rows, four re-anchored.
This commit is contained in:
+25
-19
@@ -1971,7 +1971,14 @@ def create_app(
|
||||
members = [r for r in ring if by_rel[r].group == item.group]
|
||||
group = {"key": item.group, "k": members.index(f) + 1, "n": len(members)}
|
||||
open_now = open_marks(marks)
|
||||
cring = _compare_ring(booth, items)
|
||||
# The compare partner: the next ring item a compare can open,
|
||||
# scanning forward (usually one step) rather than resolving the
|
||||
# whole ring per render. Each rel is judged once; this item is
|
||||
# re-judged first, so a race costs the control, never the page.
|
||||
partner = None
|
||||
if _in_booth(booth, f):
|
||||
partner = next((r for r in (ring[(pos + k) % len(ring)] for k in range(1, len(ring) + 1))
|
||||
if r == f or _in_booth(booth, r)), None)
|
||||
return templates.TemplateResponse(
|
||||
request, "view.html", {
|
||||
**common,
|
||||
@@ -1993,12 +2000,12 @@ def create_app(
|
||||
"is_last": pos == len(ring) - 1,
|
||||
"tray": [x for x in film if x["flagged"]],
|
||||
"back_url": f"/b/{quote(name, safe='')}/#item-{item.url}",
|
||||
# R3 C2: this item against the NEXT in the compare ring
|
||||
# (itself in a ring of one), keyed by rel like every compare
|
||||
# URL. This item passed the containment check above, so it
|
||||
# is in the compare ring.
|
||||
# R3 C2: this item against the NEXT comparable item in the
|
||||
# ring (itself in a ring of one), keyed by rel like every
|
||||
# compare URL; None, and no control, when it is not itself
|
||||
# comparable any more (it vanished after the check above).
|
||||
"compare_url": (f"/b/{quote(name, safe='')}/compare?a={quote(f, safe='/')}"
|
||||
f"&b={quote(cring[(cring.index(f) + 1) % len(cring)], safe='/')}"),
|
||||
f"&b={quote(partner, safe='/')}") if partner else None,
|
||||
})
|
||||
|
||||
# .md renders, .txt/.log show as text — viewable in-booth, no download
|
||||
@@ -2039,16 +2046,15 @@ def create_app(
|
||||
bug hunt, 3 of 4)."""
|
||||
return [r for r in review_chain(items) if _in_booth(booth, r)]
|
||||
|
||||
def _compare_side(booth: Path, ring: list[str], rel) -> str:
|
||||
"""One side of a compare, or a 404 (R3 C1). A CONJUNCTION: the view
|
||||
route's resolve / containment / is_file check, AND membership of the
|
||||
review ring. The ring alone is not enough — `booth_items` follows
|
||||
symlinks, so a link pointing outside the booth is IN the ring and only
|
||||
containment refuses it. The view's check alone is not enough — it
|
||||
renders a doc, and compare takes media only."""
|
||||
if not _in_booth(booth, rel):
|
||||
raise HTTPException(status_code=404, detail="no such item")
|
||||
if rel not in ring:
|
||||
def _compare_side(ring: list[str], rel) -> str:
|
||||
"""One side of a compare, or a 404 (R3 C1): membership of the COMPARE
|
||||
ring, which is already the conjunction — the review ring (a doc is not
|
||||
in it) filtered by the view route's containment (an outside symlink is
|
||||
in the review ring and not in this one). Judged against the ONE ring
|
||||
the route builds per request: resolving a rel twice lets a file that
|
||||
vanishes between the two reach a `.index()` that raises, a 500
|
||||
(booth-dev, after the merge)."""
|
||||
if not isinstance(rel, str) or rel not in ring:
|
||||
raise HTTPException(status_code=404, detail="no such item")
|
||||
return rel
|
||||
|
||||
@@ -2073,9 +2079,9 @@ def create_app(
|
||||
"""
|
||||
booth = resolve_booth(name)
|
||||
items = booth_items(booth)
|
||||
a = _compare_side(booth, review_chain(items), a)
|
||||
b = _compare_side(booth, review_chain(items), b)
|
||||
ring = _compare_ring(booth, items)
|
||||
ring = _compare_ring(booth, items) # built ONCE; every rel judged once
|
||||
a = _compare_side(ring, a)
|
||||
b = _compare_side(ring, b)
|
||||
# A look records both — below the 404s, so only a real pair counts.
|
||||
record_view(booth)
|
||||
record_seen(booth, a, items)
|
||||
|
||||
Reference in New Issue
Block a user