Files
DumiFlex 49f882f131 perf(api): make the drift poll conditional and stop rebuilding it
`drift-store.ts` polls /wp/api/modules/hashes and /wp/api/bundles/hashes every
5 seconds. Both the SPA and the in-graph WP_Context subscribe, so a ComfyUI tab
with a Context node on the graph polls these whether or not the manager is ever
opened.

Measured against a live 2.13.1 install on 2026-08-06: 33,008 bytes per poll, no
ETag, no Cache-Control, no Last-Modified, and 0 of 39 observed requests served
from cache. That is ~24 MB/hour per open tab on an idle page. Invisible on
localhost; over Tailscale it was the largest thing this extension does.

Producing it was not cheap either, despite the docstring's "lightweight ...
without measurable cost" -- that described the response, not the work.
`ModuleRepository.list()` is `SELECT *` plus a json.loads of every payload in
the library, and the handler kept three fields per row and discarded the rest.

Two changes:

- Conditional responses. `json_ok_revalidated` emits an ETag plus
  `Cache-Control: no-cache` -- store it, but revalidate -- and returns a bodied
  304 when `If-None-Match` matches. Nothing changes for the client: the browser
  serves the cached body as a 200, so `fetch().then(r => r.json())` is
  untouched and the saving is entirely on the wire.

- A cheap stamp behind the ETag. `library_stamp()` is
  COUNT/SUM(version)/MAX(updated_at) -- one aggregate row, no payloads. The
  expensive map is built only when the stamp moves, which is the whole point:
  computing an ETag from the response body would have done all the work anyway
  and then thrown it away.

Also narrows the map builders. `list_hash_rows` selects the columns the drift
map needs instead of every column via `_row_to_module`/`_row_to_bundle`, which
parse `tags` (and `children`, for bundles) per row for a map that wants
neither. Bundles get the bigger win -- `payload_hash` is a stored column there,
so that path now parses nothing at all. Modules still parse `payload` because
their hash is computed rather than stored; removing that needs a schema
migration, and the stamp makes it moot in the steady state.

Stamp precision, both directions verified: `set_community_origin` writes
without a version bump and cannot change the map, so the ETag correctly holds.
A favourite toggle goes through `update()`, whose single statement always does
`version = version + 1`, so it does move the ETag and costs one refetch of an
identical map. Accepted rather than fixed -- a rare user action against a second
counter tracking payload-affecting columns only -- and pinned by a test that
says so, and says a future failure would be an improvement.
2026-08-06 04:12:44 +03:00

260 lines
9.7 KiB
Python

"""Conditional-request behaviour for the two drift-poll endpoints.
`components/context/drift-store.ts` polls `/wp/api/modules/hashes` and
`/wp/api/bundles/hashes` every 5 seconds, and BOTH the SPA and the in-graph
WP_Context subscribe — so a ComfyUI tab with a Context node on the graph polls
these forever whether or not the manager is ever opened.
Measured against a real install on 2026-08-06: 33 KB per poll, no caching
headers, nothing served from cache. These tests pin the fix — a 304 on an
unchanged library, and an ETag that moves when (and only when) a payload does.
"""
from __future__ import annotations
import sqlite3
import pytest
from aiohttp import web
from engine.db.migrations import migrate
from engine.db.repositories import BundleRepository, ModuleRepository
from wp_api import bundles as bundles_api
from wp_api import modules as modules_api
@pytest.fixture
def app_with_db():
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
migrate(conn)
app = web.Application()
app["wp_db"] = conn
modules_api.register(app.router)
bundles_api.register(app.router)
return app, conn
def _make_module(conn, name="x", payload=None):
return ModuleRepository(conn).create(
type="wildcard", name=name, description="",
category_id=None, tags=[], payload=payload or {"options": []},
)
class TestModuleHashesConditional:
async def test_first_response_carries_an_etag_and_revalidate_header(
self, aiohttp_client, app_with_db,
):
app, conn = app_with_db
_make_module(conn)
client = await aiohttp_client(app)
resp = await client.get("/wp/api/modules/hashes")
assert resp.status == 200
assert resp.headers["ETag"]
# `no-cache` means "store, but revalidate" — NOT "don't store". The
# whole saving depends on the browser keeping the body.
assert resp.headers["Cache-Control"] == "no-cache"
async def test_unchanged_library_answers_304_with_no_body(
self, aiohttp_client, app_with_db,
):
app, conn = app_with_db
_make_module(conn)
client = await aiohttp_client(app)
first = await client.get("/wp/api/modules/hashes")
etag = first.headers["ETag"]
second = await client.get(
"/wp/api/modules/hashes", headers={"If-None-Match": etag},
)
assert second.status == 304
assert second.headers["ETag"] == etag
assert await second.read() == b""
async def test_editing_a_payload_moves_the_etag(
self, aiohttp_client, app_with_db,
):
app, conn = app_with_db
row = _make_module(conn)
client = await aiohttp_client(app)
etag = (await client.get("/wp/api/modules/hashes")).headers["ETag"]
ModuleRepository(conn).update(row["id"], payload={"options": [{"value": "a"}]})
resp = await client.get(
"/wp/api/modules/hashes", headers={"If-None-Match": etag},
)
assert resp.status == 200, "a payload edit must not be served as 304"
assert resp.headers["ETag"] != etag
async def test_inserting_and_deleting_move_the_etag(
self, aiohttp_client, app_with_db,
):
app, conn = app_with_db
_make_module(conn, name="first")
client = await aiohttp_client(app)
etag_one = (await client.get("/wp/api/modules/hashes")).headers["ETag"]
extra = _make_module(conn, name="second")
etag_two = (await client.get("/wp/api/modules/hashes")).headers["ETag"]
assert etag_two != etag_one, "insert must invalidate"
ModuleRepository(conn).delete(extra["id"])
etag_three = (await client.get("/wp/api/modules/hashes")).headers["ETag"]
assert etag_three != etag_two, "delete must invalidate"
async def test_community_origin_write_does_not_move_the_etag(
self, aiohttp_client, app_with_db,
):
"""The stamp must not move for a write that cannot change the map.
`set_community_origin` writes two metadata columns with raw SQL and
deliberately does NOT bump `version`, so the drift map is
byte-identical afterwards. Holding the ETag steady is correct — moving
it would cost every open tab a refetch of a map it already has.
"""
app, conn = app_with_db
row = _make_module(conn)
client = await aiohttp_client(app)
before_resp = await client.get("/wp/api/modules/hashes")
before_etag = before_resp.headers["ETag"]
before_body = await before_resp.json()
ModuleRepository(conn).set_community_origin(
row["id"], post_slug="some-slug", version_number=2,
)
after_resp = await client.get("/wp/api/modules/hashes")
assert await after_resp.json() == before_body, (
"precondition: this write must not alter the drift map"
)
assert after_resp.headers["ETag"] == before_etag
async def test_favorite_toggle_does_move_the_etag_and_that_is_accepted(
self, aiohttp_client, app_with_db,
):
"""Documents a known imprecision rather than asserting it away.
Favouriting goes through `ModuleRepository.update`, whose single
UPDATE statement always does `version = version + 1`. So the stamp
moves and every open tab refetches — even though `payload_hash` did
not change and the map is identical.
Left as-is deliberately: the cost is one extra fetch on a rare,
user-initiated action, and the alternative is a second version counter
tracking payload-affecting columns only. If this test ever starts
failing because the ETag held steady, that is an IMPROVEMENT — verify
the map is genuinely unchanged and update the expectation.
"""
app, conn = app_with_db
row = _make_module(conn)
client = await aiohttp_client(app)
before_resp = await client.get("/wp/api/modules/hashes")
before_etag = before_resp.headers["ETag"]
before_body = await before_resp.json()
ModuleRepository(conn).update(row["id"], is_favorite=True)
after_resp = await client.get("/wp/api/modules/hashes")
assert await after_resp.json() == before_body, "the map itself is unchanged"
assert after_resp.headers["ETag"] != before_etag
async def test_body_is_unchanged_from_the_unconditional_shape(
self, aiohttp_client, app_with_db,
):
"""`list_hash_rows` replaced `list()`; the wire shape must not drift."""
app, conn = app_with_db
wc = _make_module(conn, name="wc")
cb = ModuleRepository(conn).create(
type="combine", name="cb", description="",
category_id=None, tags=[], payload={"template": "$a"},
)
client = await aiohttp_client(app)
body = await (await client.get("/wp/api/modules/hashes")).json()
assert body["hashes"][wc["id"]] == {
"type": "wildcard", "payload_hash": wc["payload_hash"],
}
assert body["hashes"][cb["id"]] == {
"type": "combine", "payload_hash": cb["payload_hash"],
}
class TestBundleHashesConditional:
async def test_unchanged_library_answers_304(
self, aiohttp_client, app_with_db,
):
app, conn = app_with_db
BundleRepository(conn).create(
name="b", description="", color=None,
category_id=None, tags=[], children=[],
)
client = await aiohttp_client(app)
first = await client.get("/wp/api/bundles/hashes")
assert first.status == 200
etag = first.headers["ETag"]
second = await client.get(
"/wp/api/bundles/hashes", headers={"If-None-Match": etag},
)
assert second.status == 304
assert await second.read() == b""
async def test_new_bundle_moves_the_etag_and_body_shape_holds(
self, aiohttp_client, app_with_db,
):
app, conn = app_with_db
repo = BundleRepository(conn)
first_bundle = repo.create(
name="b", description="", color=None,
category_id=None, tags=[], children=[],
)
client = await aiohttp_client(app)
etag = (await client.get("/wp/api/bundles/hashes")).headers["ETag"]
repo.create(
name="c", description="", color=None,
category_id=None, tags=[], children=[],
)
resp = await client.get(
"/wp/api/bundles/hashes", headers={"If-None-Match": etag},
)
assert resp.status == 200
body = await resp.json()
# Flat `{id: hash}` — bundles differ from modules here, deliberately.
assert body["hashes"][first_bundle["id"]] == first_bundle["payload_hash"]
class TestIfNoneMatchParsing:
async def test_weak_prefix_and_multi_value_lists_are_honoured(
self, aiohttp_client, app_with_db,
):
"""Proxies rewrite and merge these headers; both forms are legal."""
app, conn = app_with_db
_make_module(conn)
client = await aiohttp_client(app)
etag = (await client.get("/wp/api/modules/hashes")).headers["ETag"]
weak = await client.get(
"/wp/api/modules/hashes", headers={"If-None-Match": f"W/{etag}"},
)
assert weak.status == 304
listed = await client.get(
"/wp/api/modules/hashes",
headers={"If-None-Match": f'"other", {etag}'},
)
assert listed.status == 304
async def test_a_stale_etag_gets_a_full_response(
self, aiohttp_client, app_with_db,
):
app, conn = app_with_db
_make_module(conn)
client = await aiohttp_client(app)
resp = await client.get(
"/wp/api/modules/hashes", headers={"If-None-Match": '"stale"'},
)
assert resp.status == 200
assert "hashes" in await resp.json()