A session hosted with `bind` 0.0.0.0 (or ::) kept listening on every
interface but put 0.0.0.0 in its edit and view links, URLs, join codes
and shown address, which nobody can connect to. They now name this
machine's local network address of the listener's family (loopback
without one), on the same port.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6DxbDYxuPEA2pJirCLQ
The previous fix made a local share's `link` follow the code shown, but its
top-level `url`, the session's WebSocket URL, still carried the edit token:
`collab.join {link: url}` from a view-only share joined with edit access and
the edit reached the host. The link layer now hands over the view way in as a
link and its URL (`SessionInfo::view`, a `WayIn`, in place of `view_link`), and
`collab.peers` gives a local share's `link` and `url` from the way in for the
code shown (`Live::invite`). Online shares are unchanged.
Regression test: `code_tests::a_local_view_share_s_link_and_url_only_let_people_look`
joins through the `link` and then the `url` over loopback; both are welcomed as
viewers whose edits never reach the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2VLhC9d5v8VSsf4ng
`collab.share {via: "local", access: "view"}` showed the view-only code but
gave the session's own invite link as `link`, and that link edits: whoever
joined through it could change the host's file. A local share's `link`, top
level and in `sessions`, is now the one that goes with the code shown, and it
follows `collab.share {access}`. Online shares keep the owner's own link there,
as before.
Regression test: `code_tests::a_local_view_share_s_link_only_lets_people_look`
joins through that link over loopback and is welcomed as a viewer whose edit
never reaches the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2VLhC9d5v8VSsf4ng
The session the app hosts for a local network share now has two secrets:
the edit code and a view-only code (same address and port, a 44-bit secret
of its own). Whoever joins with the view code is welcomed as a viewer: they
see every change and presence, the app makes the file read-only for them,
and the session refuses their batches, as a server does for view links.
Share > Local network offers Can edit / Can view before sharing starts (the
edit code then never shows) and while sharing. collab.share {via: "local",
access: "view"} does the same; share in collab.peers lists both codes and
links. There is still no comment code on the local network.
Next::Stop boxes its event: SessionInfo grew past clippy's
large_enum_variant threshold, and it only ever carries a status or an end.
Tests: loopback_tests::a_view_token_lets_people_look_and_not_edit,
code_tests::a_local_view_code_lets_people_look_and_not_edit,
live_share_tests::a_local_share_can_start_for_looking_only, and the local
share tests that asserted edit-only codes. Docs: collab-protocol.md,
collaboration-parity.md.
A queue past what a server takes at once would get the connection refused before
the end of it arrived. The desktop keeps sending at the pace for up to 10 s on its
own thread, and the browser sends what may go now. `collab.reconnect` also respects
a refusal of network access while it waits to reconnect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pace was set when a message was queued, so messages held up by a stalled
connection left together afterwards and could trip the server's rate limit. One
queue now counts it as they go, for the desktop and the browser alike.
Leaving while changes are unconfirmed now says so: an undo waits for its edit, so
what was still on its way may not all reach the others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client taking in a large welcome holds one queue slot for the whole transfer,
while everyone's presence fills the rest. Five people moving their cursors fill
1024 slots in about 7 s, and the joiner is dropped with the same result on every
try. Presence now stays out of a queue that is half full: it is latest-wins, and
the other half stays free for edits, which can't be left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Queued sync messages wait in a list the session loop drains, so Leave, a lost
connection, presence and pings are never held up behind them. A text frame from
the host stays final. A session that ended stays on its tab after a Save as:
ending it there could turn a tab into a plain local file with no word about it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session you joined could end for reasons that pass, or after about 85 s of
retries, and its tab then looked like an unsaved local file. Work done there
reached no one, with only a 3 s toast to say so.
A live connection now reconnects until you leave, pausing a minute after each
12 attempts. Only refusals that are for good stop it, so "too many messages",
"busy" and the like retry. Messages queued while offline go out paced so a long
queue can't trip the server's rate limit. A session that ended stays on its tab:
the tab shows a red mark and a strip across the canvas offers Retry now or Try
again, Save a copy and Dismiss. Joining again uses the same tab, and what changed
offline opens beside it as an unsaved copy. A session that gets out of step
joins again by itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reply read until the server answered, and every ping, pong or control
frame renewed the per-read timeout, so a library endpoint could hold a
background refresh (and the shared refresh permit) open indefinitely. The
whole exchange now ends at one absolute deadline, the idle timeout from
when the request went out.
The request line was read within the hello timeout, but the header loop
after it only timed out per read, so a peer dripping header bytes kept the
worker thread and its connection slot for as long as it liked. Each header
read now gets what is left of the one deadline.
* M3: library analytics list how each style and variable is used
A file reports how many of its layers use each of a team library's styles and
variables, and Show usage lists them by a Type choice with their layers and
files. Layers in instances count for their component. Also ignores plugins/target.
* M3: the style analytics test waits for the instance report too
Gap 10: a file counts the library instances it places, creates, pastes or duplicates and reports them like its detaches. The server counts what a file's totals grow past their highest in the current week and keeps a year of weeks (actions.json). library.usage {days} and Show usage give insertions and detachments over the past 30 days, 90 days or year. New variants are appended; an older server refuses them and Show usage says so.
* M3: team library analytics count detached instances
Detaching an instance of a library component counts it in the file (libraries.detached, saved with it, capped). The background library check reports the counts after its fetch (ReportDetached); the server keeps them per file beside the file's usage report in usage.json (same caps) and answers Detached with them. Show usage and library.usage list detaches per component and per file (detached, detachesKnown). The new request and reply variants are appended, so every older library message keeps its bytes; an older server refuses the new requests, which the app ignores. Gap 10.
* M3: the detach test waits for the instance count too (the two arrive in separate reports)
* M8: cap the people a client keeps track of and the deleted ids a session remembers (#1121)
A hostile host or server could grow a client's presence, seen-peers and who-is-here lists without bound, and a session's deleted-id set only grew. Clients keep at most MAX_PEERS (4,096) others, leaving newcomers out with a warning; max_people is at most that; a session remembers at most 262,144 deleted ids, the lowest forgotten first.
* M8: a max_people above what clients track is lowered with a warning, not refused (#1121)
Refusing it would stop a server whose existing config says more from starting after an upgrade. Config::people() lowers max_people to MAX_PEERS for the core's limits, and the startup warnings name the configured and the used value.
* M8: the live welcome's handling in its own function (Sonar S3776)
The per-address download bucket never held more than its 16 MiB burst, so a larger library or plugin was refused for good. Bucket::take_spread serves one from a full bucket and leaves the debt to refill at the rate before the next download; the rate over time still holds.
Someone who joins with a comment link (or its join code) sees the file live and adds, replies to, reacts to and resolves comment threads, but can't change the design: their app refuses other edits and the server accepts a comment link's batch only when every change in it touches comment threads. Sharing online makes a comment link and code beside the edit and view ones; the Share popover offers Can edit, Can comment and Can view, and the commenter's toolbar shows a Can comment badge with Move, Hand and Comment.
Backward compatible: clients announce the comments word; one that doesn't is welcomed with a comment link as a viewer and gets comment links listed as view links, a level this build doesn't know reads as view, and an older server that can't make a comment link leaves the share without one.
The web transport implements LiveLink::publish and the hosting and sharing background requests over the page's WebSocket. Managing connections run as exchanges (exchange.rs) in the browser client's driver; the requests and what their answers mean are shared with the desktop transport (requests.rs). The Share popover offers online sharing in the browser (no local network), and Publish to server opens there. Fixes#917.
* M9: one name for comments and live sessions, shown in the Join dialog, kept in the browser, renamed live (#1380)
* M9: a reconnecting session's reset is its own helper (#1380)
* M9: a per-address budget for server admin requests, and join codes found by hash (#1075)
* M9: the server tests share one snapshot and address helper (#1075)
A viewer moving the pointer sends presence about 30 times a second, and every message sent put
the next ping off by the keep-alive interval, so no ping went out while presence did. The host
answers pings and nothing else to a client alone in a session (presence goes to the other peers),
so nothing was heard for the idle timeout (45 s): the client dropped a healthy connection and
reconnected, over and over. Seen in the browser build; the desktop app's guest has the same
timing (its reader times out the same way) and gets the same change.
Pings are now timed from the last ping, not from the last message sent: one goes out every 15 s
whatever else is on the wire. Regression test: 90 s of presence at 10 a second with the host
answering only the pings keeps the connection (it closed before the change).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A publish uploads a whole file; a large project on a slow link takes the
server a while to receive and answer. But the server's one-shot request
connections (manage, hosting, library, plugins) used the short request_idle
as their read timeout, so a slow upload could be cut off mid-processing. The
manage/hosting connections that carry uploads now wait the longer
request_reply_timeout (as the client already did), keeping the short idle
for the other one-shot requests and sessions.
Co-Authored-By: Claude <noreply@anthropic.com>
* E21: require release font coverage and stabilize capture deadline tests
* E21: separate capture, IME and field-save regression phases
* M0: correct the migration qualification reference
* M-egui037: document input lifecycle and UI regression contracts
* M0: correct egui 0.37 status docs and stabilize join-code regression
---------
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
On Windows a socket's read timeout counts whole milliseconds, so a read
bounded by the hello deadline can time out a moment before it.
`next_message` then took the timeout for a closed connection: the client
got no "no hello in time" refusal, and the socket closed only after the
2 s linger. `Duplex::read` now reads again until the deadline has passed.
Tests:
- `connections_without_a_hello_are_told_why_they_end` (new): twelve
connections that never say hello each get the refusal. Failed 5 of 10
runs without the fix.
- `pre_hello_lifetime` notes the end of the stream on a thread of its own
instead of reading one message per 300 ms poke, where pongs queued
ahead of the close delayed it.
- `quiet_guests_stay_and_silent_connections_are_dropped`: the guest takes
only the host's keepalive. Its 600 ms idle timeout also bounded the wait
for the welcome.
- `two_files_are_hosted_and_joined_independently`: the two joins race, so
the file whose welcome arrived last is in front. The test checks that,
then brings B to the front.
- `a_local_share_is_joined_by_its_code_and_edits_sync`: once the share
stops, another program's host can take the freed port and refuse the
code.