diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-16 15:29:11 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-16 15:29:11 +0200 |
| commit | 84e778d5cdd1bb5cded8a7c0238797c17d48666c (patch) | |
| tree | d26f1d20735ba2db81da812371c92d257179882c /packages/meshbay-hub/tests/test_downloads.py | |
| parent | 188a76f52d2f30609147b1beee7754f2cbd1e778 (diff) | |
| download | meshbay-84e778d5cdd1bb5cded8a7c0238797c17d48666c.tar.gz | |
feat(hub): chat, presence, a Profile page, and downloads that do not freeze
Chat opens on the newest hundred messages, loads fifty older on demand with the
reading position anchored — the distance from the *bottom*, since everything
above the viewport just grew — and follows new messages only when the reader was
already at the end. Day separators, sender grouping, an unread marker, and a
jump-to-latest pill. Messages are keyed by id: index keys plus prepending makes
Preact reuse the wrong bubbles.
A presence dot per group in the sidebar, three states, each backed by something:
the hub's registry, or a connection this browser made or failed to make. Never
colour alone — red and green are the pair colour-blind readers cannot separate —
so each dot carries a title and an aria-label.
Profile is split out of Settings: identity, node link, pinned node identities and
account deletion. Mixing them put an irreversible button two scrolls under a
theme picker. The create-group page loses its centred 520 px card, which left
190 px of margin either side, and its two button panels become a radio group —
a button conveys no chosen state to a screen reader, and side by side they read
as two independent actions rather than one either/or.
The Files toolbar shows its actions as icon buttons the moment Select is on,
disabled when they do not apply rather than appearing and vanishing. On a phone
the right-hand group could not wrap and ran 130 px off the screen.
Streamed downloads no longer freeze after one chunk. `registration.active` says
a worker exists, not that this page is controlled by it — and an uncontrolled
page's requests never reach its fetch handler, so the worker took the stream and
was never asked for it, leaving `writer.write()` waiting on backpressure that
would never lift. The page now requires control and the worker confirms it
actually served the request before the sink is trusted.
Fixed on the way: `setActionsOpen` outlived the state it belonged to and threw
on every Files action; the chat scrollbar stopped short of the bottom; the
owner's row sat lower than the rest; About showed a version hardcoded two
releases ago.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_downloads.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_downloads.py | 41 |
1 files changed, 40 insertions, 1 deletions
diff --git a/packages/meshbay-hub/tests/test_downloads.py b/packages/meshbay-hub/tests/test_downloads.py index 41d61bf..85bf488 100644 --- a/packages/meshbay-hub/tests/test_downloads.py +++ b/packages/meshbay-hub/tests/test_downloads.py @@ -161,6 +161,45 @@ def test_backpressure_is_real(tmp_path): src = DOWNLOADS.read_text() fn = src[src.index("export async function openStreamedDownload"):] assert "new TransformStream()" in fn - assert "[readable]" in fn, "the readable half must be transferred, not copied" + # The transfer list may carry more than the stream — a reply port rides + # along now — so this asserts that `readable` is transferred, not the exact + # shape of the list. + transfer = fn[fn.index("worker.postMessage("):] + transfer = transfer[transfer.index("["):transfer.index("]") + 1] + assert "readable" in transfer, "the readable half must be transferred, not copied" assert "writer.write(bytes)" in fn assert "return null" in fn, "a browser that cannot transfer streams must say so" + + +def test_the_streamed_path_gives_up_rather_than_blocking_for_ever(): + """Reported 2026-08-16: a download frozen at exactly one chunk. + + The writable half applies real backpressure, which is the whole point — and + the trap. If nothing ever reads the readable half, `writer.write()` waits + for room that never comes, and the transfer stops dead after the stream's + internal queue fills. Two ways that happens on a phone: the page is not yet + *controlled* by the worker, so the iframe's request is never handed to its + fetch handler; or the browser refuses a download started from a hidden + iframe. Both are silent. + + So the worker confirms that it actually answered, and this path reports + failure instead of returning a sink nobody drains. + """ + src = DOWNLOADS.read_text() + fn = src[src.index("export async function openStreamedDownload"):] + assert "mbdl-serving" in fn, "the worker has to confirm it served the request" + assert "Promise.race" in fn, "the confirmation needs a deadline" + assert "writable.abort" in fn, "give up cleanly so the caller can fall back" + + sw = (DOWNLOADS.parent / "sw.js").read_text() + assert "mbdl-serving" in sw, "and the worker has to send that confirmation" + + +def test_an_uncontrolled_page_is_not_treated_as_ready(): + """`registration.active` says a worker exists, not that it will see our fetch.""" + src = DOWNLOADS.read_text() + fn = src[src.index("async function serviceWorker()"):] + fn = fn[:fn.index("\n}")] + assert "navigator.serviceWorker.controller" in fn + assert "controllerchange" in fn, ( + "control can arrive a tick after registration; waiting beats refusing") |