feat(relay): phone-side frame forwarder (#25)#28
Merged
Merged
Conversation
Adds StartPhoneForwarder, the per-phone read pump that wraps each inbound phone frame in the routing envelope keyed by the phone's relay-assigned conn_id and writes it to the binary holding the server-id. Replaces the placeholder CloseRead + <-readCtx.Done() block in /v1/client. The handler's existing defer (UnregisterPhone, Close) is unchanged; the forwarder is purely a read-pump and never touches registry or conn lifecycle. Adds WSConn.Read as the production-side phoneSource implementation; documents the single-reader contract. Tests cover bytewise-opaque round-trip (canonicalised via json.Compact per the whitespace lesson), phone disconnect, missing binary, and ctx cancellation. forward_test.go's package doc records the manual stress invocation per the race-count lesson. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
Author
Code Review: #25Decision: PASS Findings
SummaryImplementation is a faithful translation of the architecture doc:
Security review:
CI: |
Adds the phone-forwarder feature doc covering the synchronous read pump, the four termination paths, and the handler contract (forwarder is read- only; cleanup is the handler's defer). Updates client-endpoint and ws-conn-adapter to reflect the CloseRead → StartPhoneForwarder swap and the new single-caller WSConn.Read. Sharpens the existing CloseRead lesson to call out that the placeholder must be deleted (not retained) once the real reader lands.
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.
What
Adds
StartPhoneForwarder, the per-phone read pump that wraps each inbound phone frame in the routing envelope keyed by the phone's relay-assignedconn_idand writes the wrapped envelope to the binary holding the server-id. Replaces the placeholderc.CloseRead(...) + <-readCtx.Done()block in/v1/clientwith a real frame loop.Adds
WSConn.Readas the production-sidephoneSourceimplementation; documents the single-reader contract on the type-level doc comment.Issue
Closes #25.
Testing
internal/relay/forward_test.gocovers four scenarios against a realRegistrywith package-private fakes (fakePhonefor the read source,fakeBinaryfor the registry-sideConnwith mu-protectedsent):ConnIDand inner-frame opacity viajson.Compactper the whitespace lesson.io.EOF, then mimics the handler-levelUnregisterPhoneand assertsPhonesForreturns nil.phone_forwarder_no_binaryand returnsnilon the first frame; no panic.cancel()withcontext.Canceled.Manual stress invocation is documented in
forward_test.go's package doc per the race-count lesson:Verified locally (
-count=20clean in 10s).go test -race ./...andgo vet ./...pass.Architecture compliance
docs/specs/architecture/25-relay-phone-forwarder.md:phoneSourceinterface defined at the consumer (forward.go), not exported, not onWSConn— fakes substitute for tests; production passes*WSConn.WSConn.Readis single-caller; does not plumbcloseCtxbecause*websocket.Conn.Closealready aborts in-flight reads.CloseRead + <-readCtx.Done()entirely (does not retain it alongside the new reader — seelessons.mdline 13-15).UnregisterPhoneorClose; the handler's existing defer owns cleanup.🤖 Generated with Claude Code