diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-09 16:37:23 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-09 16:37:23 +0200 |
| commit | 391db2f2197b5f7fbdba0c918a24830a5cbe6ee4 (patch) | |
| tree | 570480f075de3b8f6609675b9ced67815771d961 /docs/apps.md | |
| parent | 56776934c2ddfbad4884b252da6f5fb25864b8db (diff) | |
| download | meshbay-391db2f2197b5f7fbdba0c918a24830a5cbe6ee4.tar.gz | |
fix(spa): a reconnect must give the Chat composer back
The Chat tab froze about every other day — the textbox stopped taking clicks —
and it never recovered on its own: no timeout ends this one, only leaving the
group or restarting the client. A console dump of a session it happened in ruled
out everything it could and named nothing.
What that dump established was almost entirely negative, and that was the useful
part. No `Response timeout`, no `unsolicited`/`unrouted`/`with nothing waiting`
— so the 2026-08-30 routing defect, which produces this exact symptom for thirty
seconds, had not recurred. No `PC state: disconnected|failed`, no second ICE
cycle, no `Reconnected after N attempt(s)` — so the connection was alive and
untouched. The freeze was in the page, and no path that logs anything had run.
The composer is `disabled=${sending || cannotSend}`, and `cannotSend` was
`transport.connected && !transport.devicePk`, read off a **ref** during render.
`devicePk` is settled inside connect(), so every reconnect clears it and settles
it again; a ref changing re-renders nothing, and nothing else announced it. So
the panel went disabled on whatever unrelated re-render came next — a message
arriving — long after the identity was actually lost, and had no event that
would open it again. group-page.js never touches `status` after 'connected', and
`onReconnected` is claimed by video-player.js, so there was no second chance.
It was silent as well as sticky. `_announceDevice` had three exits that wrote
`devicePk` without a word: two early returns that left the *previous*
connection's value standing, and a reply that is not `device_hello_ack` — an
`error` reply does not throw, so the `.catch()` at the call site never saw it.
Reproduced in chat_send_probe.py, which mounts the real ChatPanel over the real
transport: with the old code, identity cleared leaves the composer open, an
arriving message latches it shut, and restoring the identity does not reopen it.
Every write to `devicePk` now goes through `_setDevicePk(pk, why)`, which logs,
traces and calls `onDeviceIdentity`; group-page holds the answer as state and
ChatPanel takes it as `deviceReady`. Defaulting that prop to `true` fails open —
a wiring mistake here must not be able to leave anyone with a dead textbox.
Two things found on the same path and fixed with it. `_send` throwing inside
_sendAndWait's executor left the pending entry and its 30s timer behind, so a
request that never reached the wire still logged a "Response timeout" half a
minute later. And the instrumentation this was meant to be diagnosed with
(3be8bd2) writes to localStorage behind ?trace=1, not to the console, so the
dump could not have carried it: the two lines that decide the composer's state
are now logged unconditionally, and MeshBayTrace gains `record` so the composer
writes into the same timeline as the channel events.
Hub suite 2264 passed, 4 skipped. chat_send_probe.py gains a `reconnect`
scenario and test_chat_send.py four cases, each checked against the unfixed
source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019GXmScYB1uR29YCt74si9J
Diffstat (limited to 'docs/apps.md')
| -rw-r--r-- | docs/apps.md | 3 |
1 files changed, 2 insertions, 1 deletions
diff --git a/docs/apps.md b/docs/apps.md index 7819dc9..85b39da 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -60,7 +60,7 @@ whichever app is active: ```js const commonProps = { - groupId, transportRef, gekRef, status, username, + groupId, transportRef, gekRef, status, username, deviceReady, entries, nodeDirs, nodeRoots, setEntries, setNodeDirs, setNodeRoots, applyIndex, isNodeAdmin, operatorPaired, mayUpload, userId, setError, onPreview, onRefreshIndex: refreshIndex, onActivity: touchActivity, @@ -78,6 +78,7 @@ needs — a new app does not get a bespoke prop list. Notable ones: | `applyIndex(indexMsg)` | writes a fresh index into the three above, plus the search cache | anything that mutates files (upload, delete, mkdir) calls this so every app sees the result | | `onPreview(entry)` | opens the shell's video/preview modal | `entry.type === 'video'` routes to `VideoPlayer`, anything else to `FilePreview` — an app just calls this, it does not own modal state | | `transportRef`, `gekRef` | refs to the live MNP transport and the imported group key | never state — a ref, so reconnects don't force a re-render of every app | +| `deviceReady` | whether this connection has identified a device to the node (`device_hello`) | **the exception to the row above, and why it is a prop.** A ref not re-rendering is right for a transport an app reaches into on demand, and wrong for a *fact about the connection* an app renders from. Chat's composer gates on this one: a reconnect clears it and settles it again inside `connect()`, and while it was read off `transportRef.current.devicePk` during render, the panel latched shut on whatever unrelated re-render came next and had no event that would open it again. See `test_chat_send.py` | | `mayUpload` | `memberUpload || isNodeAdmin`, computed once | Files' toolbar and Chat's composer both gate on it; a second derivation would eventually disagree with the first | ### 2b. The same app, rendered by the Search page |