<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/downloads.js, branch 0.13</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.13</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.13'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-10T15:30:40Z</updated>
<entry>
<title>docs(code): drop the references to a plan file that no longer exists</title>
<updated>2026-09-10T15:30:40Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-10T15:30:40Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d5442e3a335327b445d27d0f7e8798726ba20973'/>
<id>urn:sha1:d5442e3a335327b445d27d0f7e8798726ba20973</id>
<content type='text'>
Seven comments pointed at sections of `~/next/improve-downloads.md`, which is
not in the tree and not anywhere a reader of this repository can follow. Each
now states the thing it was citing: why a paused transfer holds nothing, why the
lease is taken after the save target and not before, why a chunk request marks
a lease alive, where the leaseless bound's number comes from.

The leaseless comment also said "two files at a time" three paragraphs under
`MAX_LEASELESS_IN_FLIGHT = 12`, left behind when the bound was raised. A comment
that contradicts the constant beside it is worse than no comment: one of them is
wrong and the reader cannot tell which.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
</content>
</entry>
<entry>
<title>fix(spa): wake the download worker before handing it a stream</title>
<updated>2026-09-09T10:43:08Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T10:43:08Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=dee57df42a525cead93fa30b4e7fa38a489d5b11'/>
<id>urn:sha1:dee57df42a525cead93fa30b4e7fa38a489d5b11</id>
<content type='text'>
Reported from Chrome: a download started while an upload was running took
thirty seconds to begin, every time. The console named it exactly —

    /_mbdl/mtty5btz-sbmgdegx  404 ()
    [MeshBay] the worker did not answer the download within 15s (attempt 1)

A 404 from the hub means the request reached the *network*: the worker looked,
found no entry for that id and let it through. So the worker was alive and
controlling the page, and the message handing it the stream had simply never
been processed.

`pending` lives in the worker's memory, and a worker with nothing to do is
terminated within tens of seconds. A WebRTC upload gives it no events at all,
so minutes of uploading leave it dead; the stream posted to it is lost,
silently, and the iframe then wakes it with nothing to find.

`mbdl-ping` already existed for this exact reason -- sent every ten seconds
*while* writing, because a streaming response does not count as activity.
Nothing sent one before *starting*. So a download now wakes the worker and waits
for the pong, and `sw.js` answers `mbdl-ready` once it has actually stored the
entry, which the page waits for before navigating: confirmed rather than
assumed. A worker that predates the ack sends nothing and the page navigates
anyway, which is what it did before.

This cause was measured and wrongly dismissed hours earlier, with an idle probe
that made the worker work between its own attempts -- it never actually slept.
A measurement that does not reproduce the conditions refutes nothing. The
harness now models a worker that is asleep: a ping wakes it, and anything else
posted while it sleeps is lost, which is what made the failure silent.

`test_backpressure_is_real` read the first `worker.postMessage` in the function
to check that the readable half is transferred rather than copied. The wake-up
put a ping in front of it, so it began inspecting a call that carries only a
port -- and kept passing. It now checks every post, each bounded by its own
call, since the keep-alive ping transfers nothing at all. Same shape as the
upload-seal contract this morning: a guard that reads "the first" stops
guarding the moment something is inserted before it.

Hub suite 851 passed. Both new cases checked against the unfixed source.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(spa): stop reloading a healthy page at boot</title>
<updated>2026-09-09T09:21:50Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T09:21:50Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d4adb140b9e7250b0f02d9651e4b9db87b74df81'/>
<id>urn:sha1:d4adb140b9e7250b0f02d9651e4b9db87b74df81</id>
<content type='text'>
Reported from Chrome: connecting to a group triggered a page refresh within
seconds, taking the WebRTC session down with it.

The boot check added with the bypass repair asked its question by *performing a
download* -- a four-byte stream through a hidden iframe. Chrome rations the
downloads a page may start without a user gesture to about three, measured: on
a first visit three consecutive attempts went served, served, refused. So the
check competed with the person's own downloads for that budget, and its answer
depended on how much of the budget was left. On a healthy page it concluded the
worker could not serve, and reloaded.

The same mistake the repair was written to fix, from the other side: paying a
capability to obtain a diagnostic.

The replacement costs nothing and asks nothing. Measured on Chrome, at document
start, before anything registers:

    first visit       controller false, registration false
    ordinary reload   controller true,  registration true
    hard reload       controller false, registration true

Being uncontrolled while an active registration already exists names a
hard-reloaded document exactly, so that is now the whole of the evidence. A
first visit is uncontrolled too and is not a bypass -- the worker is installing
and will claim the page in a moment -- which is precisely the case that was
reloading.

Four cases, each checked against the unfixed source, including that priming
performs no download at all. Hub suite 847 passed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>feat(spa): pause and resume a download, in session</title>
<updated>2026-09-09T08:48:49Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T08:48:49Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=29e93e5e553c94818cd2b4e587b0e54cfe7d8424'/>
<id>urn:sha1:29e93e5e553c94818cd2b4e587b0e54cfe7d8424</id>
<content type='text'>
Stage 7a of ~/next/improve-downloads.md: pausing within a session, on the
targets that can actually do it. Resuming across a reload is 7b.

A paused transfer holds **nothing**. Its slot goes back to the node the moment
it stops and resuming rejoins the queue at the tail, because anything else lets
one member close a node by pausing four downloads and going to lunch. So the
lease is taken inside the run loop rather than before it, and pause is refused
outright for a transfer that could not ask for another one.

Resuming is exact rather than approximate: the pipeline stops between two
chunks and never inside one, so what is on disk is always a whole number of
chunks and `fromChunk` is a verified position. The failure mode being avoided
is a file that looks complete and is quietly corrupt.

The target has to survive it, so a pause no longer reaches the `abort()` that a
failure does -- that would delete Electron's `.part` or the file just created in
the granted folder, leaving nothing to continue. And the in-memory fallback
keeps its accumulated chunks rather than starting a second array.

The button is drawn only where the target says it can. A service-worker stream
says no, in its own code and for its own reasons: the browser is already writing
an HTTP response into its own download folder, not feeding it stalls that
download where we cannot see or resume it, and an idle worker is terminated
within seconds. Firefox and Safari therefore keep cancel and get no pause, which
is the decision recorded in §6.5.

Cancelling a paused transfer ends it. A paused run is parked on a promise;
without waking it the row said "cancelled" over work that had not stopped and a
target that was still open.

Six cases, each checked against the unfixed source. Hub suite 842 passed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(spa): repair a bypassed page in seconds, not half a minute</title>
<updated>2026-09-09T08:32:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T08:32:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cb43495f998015850f34829329aa4509bd55d2cb'/>
<id>urn:sha1:cb43495f998015850f34829329aa4509bd55d2cb</id>
<content type='text'>
The repair worked but arrived too late to help: about thirty seconds after a
hard reload, by which time four downloads had been started and hung, and the
page reloading under them read as an unexplained refresh.

Two delays, both removed.

`_claimController` waited its whole control budget before asking for the claim.
A page that is uncontrolled while an active worker exists will never be claimed
on its own -- a document fetched by a hard reload is exactly that shape -- so
the fifteen seconds were spent waiting for something that was not coming. The
claim is now asked for first; waiting is the fallback, not the opening move.
Measured in the harness: 6042ms of a 6000ms budget before, milliseconds after.

And a download that starts while the self-test is still running now waits for
it rather than racing it. Otherwise the click spends both its attempts failing
on a path that is about to be repaired, which is what put four frozen rows on
screen.

Hub suite 836 passed. Both new cases were checked against the unfixed source.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(spa): repair a page the download worker cannot serve</title>
<updated>2026-09-09T08:05:30Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T08:05:30Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=fc148e185c01b2e25361c7625a67d310d2e1d288'/>
<id>urn:sha1:fc148e185c01b2e25361c7625a67d310d2e1d288</id>
<content type='text'>
Downloads on Firefox failed with "the worker did not answer the download within
15s", every time, for one operator, while the same profile driven from here
succeeded every time. Their own test sequence found it: a freshly started
browser downloaded four files out of four, twice; one Ctrl+F5 and every
attempt afterwards failed; restart, fine again; Ctrl+F5 before any attempt and
the very first one failed.

A document fetched by a hard reload is loaded with the service worker bypassed.
It can still be claimed afterwards, so `navigator.serviceWorker.controller`
comes back and every check in `_claimController` passes — but the navigations
that document starts keep missing the worker, and the hidden iframe a streamed
download needs is a navigation. On Firefox and Safari that is the only way to
write a file too large to hold in memory, so the download cannot happen at all,
for the life of the page.

Being controlled is not being servable, so priming now asks the question
directly instead of inferring it: a four-byte stream and a hidden iframe,
exactly as a real download would, torn down completely so nothing lands in the
download folder. When it goes unanswered the page reloads once, ordinarily,
which puts it back under the worker. The flag lives in sessionStorage rather
than a variable because it has to survive the reload it triggers, and because a
page that is still unservable afterwards must stop rather than loop.

Also stops telling people to change browser. The message said "use the desktop
app, or Chrome or Edge" for a state an ordinary reload undoes, on the one path
Firefox has no alternative to; all ten catalogues now say to reload first.

The hard reloads were on my instruction: the SPA's HTML is served `no-store`,
so a plain reload has always picked up a new build and Ctrl+F5 was never needed.

Hub suite 834 passed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(spa): nothing on the worker path may wait for ever</title>
<updated>2026-09-09T00:45:23Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T00:45:23Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=88de521725d8de10eb8bf956b31cf9ce70a81f33'/>
<id>urn:sha1:88de521725d8de10eb8bf956b31cf9ce70a81f33</id>
<content type='text'>
Four downloads on Firefox sat at "preparing" indefinitely, with the target queue
already bypassed there, so each opening was hanging on its own. The node journal
showed `d=0/8(q0) u=0/8(q0)` — no transfer had been asked for yet.

`_claimController` had two waits with no deadline at all,
`navigator.serviceWorker.register()` and `navigator.serviceWorker.ready`, while
SW_CONTROL_BUDGET_MS bounded only the wait that comes after them. `_swPromise`
is shared, so one unsettled wait left every download on the page suspended on
the same promise for the life of the tab.

Measured on Firefox 154, against a local 127.0.0.1 site so no hub was involved:
a worker that installs gives register() in 8ms and ready in 0ms; a worker whose
install handler rejects gives register() in 7ms and a `ready` that never settles
— still pending past ten seconds. register() resolves as soon as the
registration object exists, carrying nothing but an *installing* worker; ready
is what waits for an active one.

Every wait is now inside one budget, with two carve-outs so that a deadline
never costs a capability. A `ready` that times out while registration.active is
set is not fatal: ready may be waiting on a newer worker that cannot install
while an older one serves perfectly well. And the mbdl-claim recovery keeps its
own budget outside the deadline, because giving up there would cost Firefox the
only unbounded way it has to write a download to disk.

A deadline alone would have been a better-explained failure rather than a fix: a
registration stuck with nothing but an installing worker does not heal, and
every later visit finds the same one. So when ready times out with no active
worker, the registration is discarded and asked for once more with a fresh
budget, and the page repairs itself instead of needing developer tools.

Four cases pinned, each checked against the unfixed source. Hub suite 830
passed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(hub): keep the download worker alive, and never hang on a dead sink</title>
<updated>2026-09-08T20:53:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T20:53:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=051100ca32f72dd8f28489e1b6cb084f321b3b54'/>
<id>urn:sha1:051100ca32f72dd8f28489e1b6cb084f321b3b54</id>
<content type='text'>
A download froze part-way through, on Firefox, with an empty console and a node
that stayed perfectly healthy. Three separate measurements cleared the node
(615 MB pulled whole over MNP), the transport (three files interleaved on one
connection, 1.5 GB, all whole) and the service worker (three concurrent 150 MB
streams in real Firefox 154) — because none of them was wrong.

The empty console was the evidence. `_sendAndWait` logs every timeout, so no
chunk request had expired: the client was not waiting on the node. Of the three
awaits left on that path only one was unbounded.

**A service worker with no event for about thirty seconds is terminated**, and
`respondWith(new Response(stream))` does not extend its life while the response
is still being written. The reader vanished mid-file and `writable.write()` then
never resolved and never rejected — no error, no log, no failed transfer, just a
progress bar that stops. The first stress probe wrote 450 MB in two seconds and
passed: fast enough to hide it entirely.

Measured in Firefox 154, writing 1 MB every 2 s: without the ping it stalled at
17 MB after 59 s; with it, 40 MB in 80 s, complete.

  - the page pings the worker every 10 s while it writes, and the worker
    answers. Receiving a message is an event, and an event resets the timer;
  - that interval stops itself after two minutes with no write. A target can be
    opened and never written to — a transfer cancelled while it waits for a slot
    never runs, so nothing calls close() or abort() — and a timer nobody clears
    pings for the life of the page. It also kept the Node test process alive for
    ever, which is the same defect wearing a louder symptom;
  - `writable.write()` is bounded at 60 s and fails with a message naming the
    chunk. That does not fix whatever stopped a sink; it turns an unexplainable
    freeze into a failed transfer that says so, which is the difference between
    a mystery and a bug report.

Also: `Content-Disposition` lost a filename to a single apostrophe.
`encodeURIComponent` leaves `'` alone and `'` is the delimiter in RFC 5987's
`filename*=&lt;charset&gt;'&lt;lang&gt;'&lt;value&gt;`, so the header became unparseable and
Firefox named the file after the URL — 449 MB of film arrived complete as
"mtsshk9w-ohqty535". `(`, `)` and `*` get the same treatment, and a plain ASCII
`filename=` rides alongside.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(hub): make the streamed download path reliable, and clean up after an abort</title>
<updated>2026-09-08T11:20:29Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T11:20:29Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3f2bb22586d3e1aef765149b555ccc8e174ce7eb'/>
<id>urn:sha1:3f2bb22586d3e1aef765149b555ccc8e174ce7eb</id>
<content type='text'>
On Firefox and Safari the service worker is the only unbounded way to write a
download to disk: neither has the File System Access API, and OPFS is not a
substitute — measured on Firefox 154, its quota is exactly 10% of the volume's
size (389,233,459 bytes of a 3,892,334,592-byte volume, refused to the byte),
which a film exceeds. So when this path declines, a large download has nowhere
left to go, which makes its reliability a correctness property.

Four ways it declined, all of them avoidable:

- it was registered inside the first click on Download, so that click paid
  install, activate and claim while somebody watched a button do nothing;
- `_swReady` cached a null for the life of the page. One slow first click left
  the tab unable to stream anything again, curable only by a reload nobody knew
  to do. Only a successful controller is remembered now;
- control was waited for with a 3 s cap. It is 15 s, and a page that is active
  but not controlled asks the worker to claim again (`mbdl-claim`) instead of
  declaring the path unavailable;
- a missed navigation gave up at once. It gets a second attempt with a fresh id
  and iframe, the failed one torn down completely first.

Also closes a MessagePort leaked per download, and gives the reason a name
(`lastStreamFailure`) so a refusal can say what happened. The timeouts became
parameters: the defaults are the production values, no caller passes any, and
the tests do not spend a minute waiting.

`openTarget` gets an unrelated but adjacent fix, in the same file: it creates
the destination with `getFileHandle({create: true})`, so an empty file exists
before the first byte, and `abort()` leaves the target untouched — every
cancelled download left a 0-byte file behind, and since `freeName` avoids
collisions, three cancels left film.mkv, film (2).mkv and film (3).mkv, all
empty. Its `abort()` now removes the entry. Safe here and only here, because
`freeName` guarantees the name was not taken: the `showSaveFilePicker` path
must not do the same, where the person may have picked an existing file whose
contents `abort()` correctly preserves. Verified by hand in Chrome.

test_streamed_download_reliability.py runs the real module under Node against a
stubbed browser — it fails if the null is cached again.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>feat(hub): chat, presence, a Profile page, and downloads that do not freeze</title>
<updated>2026-08-16T13:29:11Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-16T13:29:11Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=84e778d5cdd1bb5cded8a7c0238797c17d48666c'/>
<id>urn:sha1:84e778d5cdd1bb5cded8a7c0238797c17d48666c</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
