Repository navigation
Conversation
…ardown BrowserSurface unmounted the Remote Surface viewer on cleanup but left its transport live, so a late input, viewport, selection read or clipboard write still POSTed to the previous stream's endpoints, and late replies still reached subscribers. A paste whose clipboard read finished after a reconnect could land in the replacement stream. Add dispose() to the PDPP transport: terminal, idempotent, drops the queued viewport and subscribers, and turns send, sendClipboardText, subscribe, receiveSseMessage and setPresentationAttachmentReady into no-ops (a late clipboard reply returns false). BrowserSurface cleanup now unmounts the viewer first, then disposes the transport in a finally block. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…eardown A request that failed after dispose() still reached the host logger, which batches events into POST /api/stream-debug, so a retired transport used the network. Gate the logger and the late catch and response paths on disposal, and skip reading a clipboard reply once disposed. BrowserSurface cleanup ran the readiness reset and mount notifications outside its try/finally, so a throwing callback skipped viewer.unmount and transport.dispose. Move the mount and cleanup into mountBrowserSurfaceViewer, whose cleanup disposes the transport in a nested finally however the notifications or the viewer unmount behave. The page still builds the viewer, so its configuration is unchanged. Add transport regressions (rejected fetch, late non-2xx, clipboard failure, no logger call after disposal) and lifecycle tests under real React: StrictMode mount, cleanup and remount, fast remount, a stale cleanup, and throwing notification or unmount. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Viewer cleanup wrote `false` into the presentation-attachment readiness ref, which the stream owns and every transport reads. Replacing a viewer (for example when the seed viewport size changes) therefore marked a still-attached stream as detached, and the replacement queued every viewport instead of posting it. Cleanup now retires only its own transport; a mount no longer writes the ref either. Mount failures were also mishandled: - A mount rejection after cleanup reached the host logger directly, which can schedule a stream-debug POST for a retired mount. - The rejection handler was registered after the host notifications, so a throwing notification left a later rejection unhandled and the partially mounted viewer alive. Registration of the rejection handler now follows mount() immediately, and a failed initialization and normal cleanup share one idempotent release. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
The mount's diagnostics logger runs inside the initialization `catch` before `release()`, and inside the mount-rejection handler. If the host logger threw, `release()` never ran and the viewer and transport leaked, and the rejection handler produced an unhandled rejection. The logger call is now wrapped so it can neither skip release nor escape. `release` also notifies the viewer unmount in a `finally` after the transport unmount notification, so a throwing transport notification no longer skips it. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When the streaming browser view closes or reconnects,
BrowserSurfaceunmounts the Remote Surface viewer but leaves its PDPP transport live. A late keystroke, viewport update, selection read or clipboard write then POSTs to the old stream's endpoints, and late replies still reach subscribers. A paste whose clipboard read finishes after a reconnect can reach the replacement stream through the shared endpoint refs.This adds
dispose()tocreatePdppRemoteSurfaceTransport(stream-viewer-remote-surface-transport.ts). After it runs, the transport is inert and idempotent:send,subscribe,receiveSseMessageandsetPresentationAttachmentReadydo nothing, andemitdrops replies already in flight.POST /api/stream-debug, so a request failing after disposal would otherwise use the network. Late rejections and non-2xx replies for input and clipboard are dropped before they are logged.sendClipboardTextreturnsfalse, including for a request that was already sent when disposal happened.The viewer mount and cleanup moved from the
BrowserSurfaceeffect intomountBrowserSurfaceViewer(stream-viewer-browser-surface-lifecycle.ts). The page still builds the viewer, so its configuration is unchanged. Cleanup unmounts the viewer and then disposes the transport in a nestedfinally, so a throwing mount notification or a throwingviewer.unmount()still retires the transport. Failed initialization and normal cleanup share one idempotent release. Diagnostics logging cannot throw out of the mount or skip release, and a throwing transport notification no longer skips the viewer notification.The mount does not write presentation-attachment readiness: the stream owns that fact (the page sets it as the SSE attachment comes and goes) and every transport only reads it. Before, cleanup wrote
false, so replacing a viewer (for example on a seed-viewport size change, which remountsBrowserSurfacewithout a new attachment) left the replacement queueing every viewport. A mount rejection now registers its handler right aftermount()and is not logged once the mount is retired.Not changed: requests already on the wire cannot be recalled; only their effects on the viewer are dropped. The
/inputschema is untouched (that is #338).Tests:
stream-viewer-remote-surface-transport.test.ts(14) andstream-viewer-browser-surface-lifecycle.test.ts(14). The lifecycle tests mount the realmountBrowserSurfaceViewerfrom a React component under jsdom: StrictMode mount, cleanup and remount; a fast remount that must not dispose the new mount's transport; a stale cleanup that must not clear the newer mount; a throwing notification or unmount; the readiness ref shared across sequential and overlapping mounts, asserting the replacement's viewport POST and ack; a mount rejection after cleanup with a recording host logger; a throwing notification during initialization with a pending mount rejection; and a throwing host logger on the initialization-failure and live-rejection paths. They do not mountBrowserSurfaceitself: it is a private component inside a 6,000-line file that pulls in CSS and the whole stream page, so the tests drive the extracted function through the sameuseEffectshape.Against the previous head (5e76f48), 4 of the new tests fail: rejected input fetch after dispose, late non-2xx input after dispose, clipboard failure after dispose (each posts to
/api/stream-debug), and a throwing notification skipping unmount and dispose. The remaining new tests are regression guards that pass on both. The 5 new readiness, late-rejection and initialization-failure tests fail against the earlier head (9ccd782) and pass now. All 14 lifecycle tests pass; the 3 added in the last commit fail against 25b55c7. The other stream-directory tests are unaffected by that commit (293 passed in the full-directory run before it);npm run types:checkinapps/consolepasses.ultracite checkshows the same findings onstream-viewer.tsxas main, minus oneuseExhaustiveDependencies(the extracted effect).stream-viewer.tsxis also edited by #620 (connector icons) at other lines (1577-1661, ~5954, ~6001); the hunks are disjoint.Assisted-by: AI