feat(js/net): share one session per relay URL - #2655
Conversation
Every connection to the same URL dialed its own WebTransport session, so a page showing N broadcasts from one relay opened N sessions. connect() now leases a shared session keyed by URL and options, lingering briefly after the last handle so a component torn down and rebuilt costs no handshake. Folds the pool and reconnect knobs into one ConnectProps used by both connect() and Reload, with pool, reload, and enabled all defaulting on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
WalkthroughAdded URL-keyed connection session pooling with reference-counted leases, grace-period reuse, abort handling, and unsafe-option exclusions. Extended 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
js/net/src/lite/publisher.test.ts (1)
99-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winName the request deadline constant.
100is a magic timeout value. Define a name such asREQUEST_DEADLINE_MSso the test documents the timing bound.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/net/src/lite/publisher.test.ts` around lines 99 - 100, In the test around live.requested(), replace the magic timeout value 100 with a clearly named REQUEST_DEADLINE_MS constant, and use that constant when creating the deadline promise so the request timing bound is documented.Source: Coding guidelines
js/net/src/connection/reload.ts (1)
16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
@internalonReloadPropshides the public constructor's option shape.
Reloadis public and takesReloadProps. Marking the whole alias@internalremoves it from the generated API documentation, so readers cannot see the accepted options from theReloadpage. Onlydelayneeds to be hidden.Move
@internalto thedelaymember and keep the alias published.♻️ Proposed doc-tag move
/** * Connection and retry options for {`@link` Reload}. - * - * `@internal` */ export type ReloadProps = ConnectProps & { /** `@internal` Superseded by `reload`, which also disables the loop. */ delay?: ReloadDelay; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/net/src/connection/reload.ts` around lines 16 - 24, Keep the public ReloadProps type alias published by removing its declaration-level `@internal` tag, and retain `@internal` only on the delay property. Preserve the existing deprecation/supersession documentation for delay so the public Reload constructor option shape remains visible while that member stays hidden.Source: Coding guidelines
js/net/src/connection/connect.ts (1)
77-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueClarify which options
connect()ignores.Line 80 says "The last three" options apply only to
Reload. This depends on declaration order inside the interface, andsignalnow sits betweendiscoveryandurl. Name the options (url,enabled,reload) so the sentence stays correct when fields move.♻️ Proposed doc tweak
- * The last three are about staying connected, so only {`@link` Reload} honors them; {`@link` connect} - * takes the URL as an argument and connects exactly once. + * {`@link` ConnectProps.url}, {`@link` ConnectProps.enabled}, and {`@link` ConnectProps.reload} are about + * staying connected, so only {`@link` Reload} honors them; {`@link` connect} takes the URL as an + * argument and connects exactly once.Also applies to: 124-141
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/net/src/connection/connect.ts` around lines 77 - 104, Update the ConnectProps documentation near connect to explicitly name url, enabled, and reload as the options honored only by Reload, replacing the order-dependent “last three” wording. Apply the same clarification to the corresponding documentation block around the additionally referenced section, without changing behavior or other option descriptions.js/net/src/connection/pool.test.ts (1)
14-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe linger assertions depend on real timer margins.
expired()waitsgrace * 3, which is 60 ms against a 20 ms grace. On a loaded CI runner asetTimeoutcan drift past that margin and the linger assertions can flake. Increase the multiplier, or use Bun's fake timers so the grace expiry is deterministic.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/net/src/connection/pool.test.ts` around lines 14 - 19, Make the linger-test expiry deterministic by updating the expired helper near the grace constant: either increase its wait beyond the current grace * 3 margin enough to tolerate timer drift, or use Bun fake timers to advance past grace without relying on wall-clock scheduling. Keep the existing grace-based linger assertions unchanged.js/net/src/connection/reload.test.ts (1)
62-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShare one pending-transport stub instead of redeclaring it.
PendingWebTransportnow appears twice in this file, once injs/net/src/connection/connect.test.tsat lines 55 to 69, and once injs/net/src/connection/pool.test.tsat lines 54 to 69. The four copies differ only in which counters they increment.Export a single stub factory from
js/net/src/mock.ts, next tocreateMockTransportPair, and have each test import it. The coding guidelines ask for duplicated helpers to be replaced with shared primitives while the surrounding code is being changed.Also applies to: 97-108
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/net/src/connection/reload.test.ts` around lines 62 - 71, Move the duplicated PendingWebTransport implementation into a shared factory in mock.ts alongside createMockTransportPair, parameterizing the close-counter behavior needed by each test. Update the PendingWebTransport setup in reload.test.ts and the corresponding implementations in connect.test.ts and pool.test.ts to import and use that factory, preserving each test’s existing counter updates.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@js/net/src/announced.ts`:
- Around line 132-135: Bound the module-scoped warnedNoDiscovery cache so
disconnected relay URLs are eventually evicted instead of retained indefinitely.
Update the warning logic that keys entries by conn.url.href to use a bounded or
connection-lifecycle-aware cache while preserving at-most-once warnings per
active relay; add regression coverage proving distinct URLs are handled and old
entries are evicted.
In `@js/net/src/connection/pool.ts`:
- Around line 272-280: The announcedBroadcast method must remove each created
announce.Broadcast from `#broadcasts` when it closes. Add a stored cleanup
callback for the watch that closes its active consumer and deletes the watch
from the tracking set, following the cleanup pattern used by announced, publish,
and consume.
- Around line 286-305: Clarify the contract of the pool handle’s closed getter
and its close method: document that closed returns the shared session lifetime
via session.closed, not when this individual lease releases its resources. Keep
the existing close cleanup and reference-release behavior unchanged.
In `@js/net/src/connection/reload.ts`:
- Around line 213-219: Update the Reload completion handling around
`#closedResolve/`#closedReject to use one shared helper that first closes
`#signals`, then settles closed with the original success or normalized error.
Apply this helper to both the reload: false branch and the retry-timeout branch,
preserving their existing resolution and rejection behavior.
In `@js/net/src/lite/publisher.test.ts`:
- Around line 94-105: Update the test teardown around publisher.runSubscribe so
the subscribe handler is retained and awaited instead of started with void.
After the request is received, complete or close the returned track to release
the handler’s track.info() wait; in finally, close the track, Publisher, and
client as appropriate, then await the runSubscribe promise to ensure all mock
resources terminate.
---
Nitpick comments:
In `@js/net/src/connection/connect.ts`:
- Around line 77-104: Update the ConnectProps documentation near connect to
explicitly name url, enabled, and reload as the options honored only by Reload,
replacing the order-dependent “last three” wording. Apply the same clarification
to the corresponding documentation block around the additionally referenced
section, without changing behavior or other option descriptions.
In `@js/net/src/connection/pool.test.ts`:
- Around line 14-19: Make the linger-test expiry deterministic by updating the
expired helper near the grace constant: either increase its wait beyond the
current grace * 3 margin enough to tolerate timer drift, or use Bun fake timers
to advance past grace without relying on wall-clock scheduling. Keep the
existing grace-based linger assertions unchanged.
In `@js/net/src/connection/reload.test.ts`:
- Around line 62-71: Move the duplicated PendingWebTransport implementation into
a shared factory in mock.ts alongside createMockTransportPair, parameterizing
the close-counter behavior needed by each test. Update the PendingWebTransport
setup in reload.test.ts and the corresponding implementations in connect.test.ts
and pool.test.ts to import and use that factory, preserving each test’s existing
counter updates.
In `@js/net/src/connection/reload.ts`:
- Around line 16-24: Keep the public ReloadProps type alias published by
removing its declaration-level `@internal` tag, and retain `@internal` only on the
delay property. Preserve the existing deprecation/supersession documentation for
delay so the public Reload constructor option shape remains visible while that
member stays hidden.
In `@js/net/src/lite/publisher.test.ts`:
- Around line 99-100: In the test around live.requested(), replace the magic
timeout value 100 with a clearly named REQUEST_DEADLINE_MS constant, and use
that constant when creating the deadline promise so the request timing bound is
documented.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: df7d2433-7b71-46a2-a24f-9c86c4d32d15
📒 Files selected for processing (12)
doc/lib/js/@moq/net.mdjs/net/src/announced.tsjs/net/src/connection/connect.test.tsjs/net/src/connection/connect.tsjs/net/src/connection/index.tsjs/net/src/connection/pool.test.tsjs/net/src/connection/pool.tsjs/net/src/connection/reload.test.tsjs/net/src/connection/reload.tsjs/net/src/ietf/publisher.tsjs/net/src/lite/publisher.test.tsjs/net/src/lite/publisher.ts
Reload settled `closed` on both terminal paths without closing its effect scope, leaving the page listeners, the probe computed, and the announce pumps behind. Route both through one helper that tears down first. The no-discovery warning cache keyed on the full href, which pins every auth token an app ever used; key on origin and path instead. Also prune a lease's announcedBroadcast handles when they close, document that a pooled handle's `closed` follows the shared session rather than the handle, publish ReloadProps' option shape via ConnectProps, and share one pending-transport stub across the connection tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keying it by relay stopped auth tokens piling up, but the set still had no eviction, so an app rotating through relays grew it for the process lifetime. Cap it and drop the oldest entry, giving up exactly-once for a hard bound: the cache exists to keep the log readable, not to guarantee a single line forever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
js/net/src/connection/reload.ts (1)
16-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDescribe
ReloadPropsby its current behavior.
@internal Superseded by ...describes migration history. The TypeScript guidelines require comments to describe current behavior. Use a current contract description instead.Proposed wording
-/** `@internal` Superseded by {`@link` ConnectProps}, which {`@link` Reload} takes directly. */ +/** Alias for {`@link` ConnectProps} accepted by {`@link` Reload}. */🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/net/src/connection/reload.ts` around lines 16 - 17, Update the JSDoc for the ReloadProps type alias to describe its current contract and relationship to ConnectProps, removing the migration-history wording about being superseded. Keep the alias itself unchanged.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@js/net/src/connection/reload.ts`:
- Around line 16-17: Update the Reload contract around ReloadProps so it does
not accept ConnectProps.transport unless Reload’s connection path explicitly
forwards and consumes that pre-existing transport. Prefer excluding transport
from the Reload input type, while preserving the other ConnectProps fields and
the existing Reload behavior.
- Around line 329-344: Update Reload.#finish to reset the public established
signal and set status to "disconnected" before closing `#signals`, while
preserving the existing closed settlement and cause handling. Add a regression
test that connects successfully, calls Reload.close(), and verifies established
is cleared and status is "disconnected".
---
Nitpick comments:
In `@js/net/src/connection/reload.ts`:
- Around line 16-17: Update the JSDoc for the ReloadProps type alias to describe
its current contract and relationship to ConnectProps, removing the
migration-history wording about being superseded. Keep the alias itself
unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f5572ca4-a727-49e1-8650-b1703ae9bb97
📒 Files selected for processing (9)
js/net/src/announced.tsjs/net/src/connection/connect.test.tsjs/net/src/connection/connect.tsjs/net/src/connection/pool.test.tsjs/net/src/connection/pool.tsjs/net/src/connection/reload.test.tsjs/net/src/connection/reload.tsjs/net/src/lite/publisher.test.tsjs/net/src/mock.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- js/net/src/connection/connect.ts
- js/net/src/announced.ts
- js/net/src/lite/publisher.test.ts
- js/net/src/connection/reload.test.ts
- js/net/src/connection/pool.ts
Reload accepts ConnectProps but never forwards `transport`, so a caller supplying a pre-existing WebTransport silently got a fresh dial instead. A supplied session is good for one connection and a reconnect loop has nothing to reuse after the first drop, so drop the option from the type rather than pretending to honor it: ReloadProps is that view of ConnectProps, published again so the constructor's shape is visible. Also cover the state a closed Reload reports, and drop two doc comments that described how the options got here rather than what they do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reload has never forwarded ConnectProps.transport, so a caller supplying a pre-existing WebTransport silently got a fresh dial. A supplied session is good for one connection and a reconnect loop has nothing to reuse after the first drop, so it stays unsupported. Warn on use instead of narrowing the type, which would break a published API and send this PR to dev for a hole that predates it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
js/net/src/announced.test.ts (1)
94-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDerive the eviction input from a named cache limit.
The literal
200does not state the cache-bound contract. Derive the handle count from a named production cache limit through a test hook, then uselimit + 1. This keeps the eviction test valid if the production bound changes.As per coding guidelines, "Avoid using magic numbers; use named constants instead."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/net/src/announced.test.ts` around lines 94 - 100, Update the eviction test around Announce.Broadcast to obtain the production cache limit through the existing test hook or an appropriate named symbol, then set the generated handle count to that limit plus one instead of the literal 200. Preserve the test’s eviction behavior while keeping it aligned with future production cache-bound changes.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@js/net/src/announced.test.ts`:
- Around line 75-85: Extend the test case around countWarnings to include
another connection with the same origin but a different connection.url.pathname,
while keeping the existing token and broadcast-path variations. Update the
expected warning count so distinct relay URL pathnames produce separate
warnings, preserving the one-warning behavior for identical relay pathnames
regardless of auth token.
In `@js/net/src/connection/reload.test.ts`:
- Around line 297-311: Strengthen the “a supplied transport is refused out loud”
test by stubbing globalThis.WebTransport with a construction counter, asserting
it remains zero after creating Reload with enabled: false, and verifying the
warning contains the expected message. Preserve the existing cleanup for Reload,
the transport pair, the warning spy, and the WebTransport stub.
- Around line 271-294: Make the teardown in the affected Reload tests
exception-safe by moving WebTransport, console.warn, and mock-transport setup
into try blocks. Track each constructed Reload and close it, awaiting its closed
promise in finally even when setup, waitUntil, or assertions fail; restore
globals and spies and close supplied transports through nested cleanup,
including constructor-failure paths.
---
Nitpick comments:
In `@js/net/src/announced.test.ts`:
- Around line 94-100: Update the eviction test around Announce.Broadcast to
obtain the production cache limit through the existing test hook or an
appropriate named symbol, then set the generated handle count to that limit plus
one instead of the literal 200. Preserve the test’s eviction behavior while
keeping it aligned with future production cache-bound changes.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b9725309-7627-4dff-8886-74234181ed67
📒 Files selected for processing (5)
js/net/src/announced.test.tsjs/net/src/announced.tsjs/net/src/connection/connect.tsjs/net/src/connection/reload.test.tsjs/net/src/connection/reload.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- js/net/src/announced.ts
- js/net/src/connection/connect.ts
- js/net/src/connection/reload.ts
The warning dedup test varied tokens and watched paths but never the relay pathname, so keying on origin alone would have passed it. The supplied-transport test asserted a warning fired without observing that a fresh dial happened, so "warned and used it anyway" would have passed too. Both now fail on those regressions. Also derive the eviction count from the cache limit rather than a literal, and close a Reload in `finally` so a failed assertion can't leave a session leased for the next case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main's fail-fast retry rewrite (#2647) landed in the same part of `#retry` this branch touches. Took its jittered, deadline-bounded backoff wholesale and re-applied the two changes here on top: the `reload: false` early return, and routing the give-up path through `#finish` so it tears down the effect scope before settling `closed`. `ReloadDelay` lives in connect.ts on this branch, so main's updated docs and its 5s/10s defaults moved with it, and `#finish` now coerces via the shared `error()` helper main introduced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Superseded by #2705, which reached the same goal one layer down. This PR pooled sessions and leased them out, which works but needs three standing rules to stay honest: Both adjacent fixes from this PR are carried forward there: the publisher stale-close guard lives in Closing unmerged. Nothing here is lost. (written by Opus 5) |
Closes #2628.
Summary
connect()dialed its own WebTransport session, so a page showing N broadcasts from one relay opened N sessions (a camera wall pays N handshakes, N congestion controllers competing with each other, and N connection slots on the relay). There was no way to fix it from outside the library:<moq-watch>'sconnectionfield is public butBroadcast/Synccapture theestablishedSignal object, so reassigning it rewires nothing.connect()now returns a reference-counted lease on a session shared by URL and options.close()releases your handle; the last one out closes the connection, after a short linger (default 2s) so tearing a component down and rebuilding it costs no handshake. Newjs/net/src/connection/pool.ts.pool: false, a suppliedtransport, or a pinned server certificate (serverCertificate/serverCertificateHashesareBufferSource | string, which no fingerprint compares safely, and sharing a session pinned to the wrong cert is not a failure worth risking).ConnectPropsbecomes the single options bag for bothconnect()andReload, absorbingurl,enabled, and a newreloadknob.pool,reload, andenabledall default to true, so<moq-watch>,<moq-publish>, moq-boy and demo/web pick this up with no element changes.announced.ts'swarnedNoDiscoverywas aWeakSet<Established>, so one lease per component meant one warning per component. Rekeyed on relay origin plus path, and capped at 64 entries dropping the oldest. Deliberately not the full href: the query carries the auth token, so keying on it would pin every token an app ever used and mint an entry per rotation. The cap trades exactly-once for a hard bound, which is the right thing to give up for something that exists to keep the log readable.Lite.Publisher.publish/Ietf.Publisher.publishremoved a path onproducer.closedwith no "is this still mine" guard (unlikeBroadcastCache.insert). Publishing a path twice on one session and closing the first producer unpublished the live one, and the path stopped answering subscribes. Latent before, reachable once two<moq-publish>elements share a session.reload: false:Reloadsettledclosedon the retry-timeout path without closing#signals, leaving thepagehide/pageshow/visibilitychangelisteners, theprobecomputed, and theannounced()pumps alive on the page. Both terminal paths now run through one#finish()helper that tears the scope down before settling, andclose()delegates to it.Design notes
The reconnect loop stays in
Reload, one per consumer; the pool shares only the session. Folding the retry loop into the pool would makedelay,webtransport,websocket,discovery, and the retry-timeout rejection onReload.closedfirst-acquirer-wins and silently ignored for everyone else. NReloads that see the same session die all retry in the same turn, so their acquires collapse onto one shared dial anyway.Two details worth a reviewer's eye:
session.closedonce and attaches.then(evict, evict).Established.closedderives a fresh promise per access andWebTransport.closedrejects on abnormal termination, so a one-argumentthenwould both leak an unhandled rejection and leave a dead session in the map for the next caller to lease.signalaborts only its own wait, never the shared attempt. When the last acquirer walks away mid-dial the attempt is aborted with no linger, and the entry is evicted synchronously so a caller arriving in the same turn starts fresh rather than joining the doomed one.Behavior changes
close()no longer terminates a session by default. It releases your handle; the connection goes when the last one does.exclude_hopon lite-04/05 (the relay filters viaselect_route) anddropReflectedis unconditionally true on lite-06+, so an app that publishes and separately watches the same broadcast over what is now one session stops seeing it and must passpool: false. Nothing in-repo relies on this:<moq-publish>'spreviewrenders the local capture or a locally decoded copy of the encoded frames, never a round trip. Documented onConnectProps.pooland indoc/lib/js/@moq/net.md. Note theAnnouncedOptions.ignoreSelfdoc still cites hang.live relying on the old behavior.Reload'senablednow defaults to true. It defaulted to false, which made a barenew Reload({ url })sit there doing nothing. Every in-repo caller passes it explicitly.stats()andprobeare now session-aggregate. Two<moq-publish>elements sharing a session would each cap their encoder at the full-sessionestimatedSendRateand together overshoot. Rare (one publish element per page is the norm) and not a regression in kind, since two separate sessions just let congestion control arbitrate. Follow-up if it bites.allowPooling: falsestays. Browser HTTP/3 pooling would merge the session with unrelated fetch traffic and losecongestionControl: "low-latency"; we share at the moq layer instead.Public API changes
All additive, so this targets
main.Connection.PoolProps({ grace?: number }).ConnectProps.pool,ConnectProps.reload,ConnectProps.url,ConnectProps.enabled.Reloadalso honorsConnectProps.signalnow, closing the whole managed connection.Reload.reloadandReload.poolfields.Announce.Broadcast.closed, a promise settling when the handle closes. It was the only closable in the package without one, and the lease needs it to prune handles it handed out.ReloadProps(it staysConnectProps, and remains the constructor's parameter type) so the accepted option shape shows on theReloadpage.ReloadDelayfromreload.tstoconnect.ts. Same exported name viaConnection.*, no consumer-visible change.ConnectProps.delayis@internal, superseded byreload. It still compiles.connection/index.ts):Pool,Entry,poolKey,sessionPool,resetPool, andcreatePendingTransportsinmock.ts.Test plan
bun --cwd js/net test: 348 pass, 0 fail. Newpool.test.ts(14 cases) covers sharing, the linger window, eviction whenclosedrejects, a shared in-flight dial, the three abort cases, every bypass, and lease cleanup. Newreload.test.tscases cover twoReloads sharing one transport,reload: false, theenableddefault, andsignal. Thereload: falsecase also asserts a laterurlchange does not dial, which only holds if the effect scope really closed.announced.test.tscovers the warning cache deduping per relay across differing tokens and evicting past its cap.a stale producer closing does not unpublish a republished path).connect.test.ts/reload.test.tsgotbeforeEach(resetPool), since they reuse URLs across cases and would otherwise lease each other's sessions.just js checkclean across all 14 JS packages.moq-doc checkfails, but identically on a clean tree (a YAML parse error indoc/.vitepress/drafts.tson a Windows checkout), so it is pre-existing and unrelated.js/CLAUDE.mdasks for on playback changes (four tiles on one relay URL showing a single session in devtools, and a DOM move across anawaitnot redialing). No nix in the environment used. Worth exercising before merge.Cross-package sync
No wire change, so no
drafts/update.rs/moq-nethas no equivalent pool and this is JS-only and additive.js/{watch,publish}public API is unchanged, so nodemo/webupdate, though demo/web's own discoveryReloadnow shares the tiles' session for free.Not included
Reloadstill acceptsConnectProps.transportand still ignores it: a supplied session is good for one connection, so a reconnect loop has nothing to reuse after the first drop. That hole predates this PR. It now warns on use rather than failing silently, which is the most that can be done without removing a field from a published type, and removing it is a semver break that belongs in its owndev-targeted PR.Issue #2627 stays open. The linger keeps the session alive across a detach, so a re-attach skips the handshake entirely, but the element still tears down its broadcast subscription (
#enabledgates watch'sBroadcasttoo), so a moved tile re-subscribes and re-renders from the next group. Closing that residual means either an element-level#enableddebounce or the same linger idea applied toBroadcastCache.(Written by Claude Opus 5)