You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As the relay, I want per-connection forwarding goroutines that read frames from each registered phone and binary, wrap them in (or strip from) the routing envelope, and write them to the addressed peer, so that phone-to-binary and binary-to-phone traffic actually flows after the WS upgrades complete.
Context
The actual routing layer. After the WS upgrades (#4, #5) register connections in the registry (#3), this ticket wires the data path: every phone frame is wrapped in the routing envelope (#1) with the phone's conn_id, and forwarded to the binary; every binary frame is unwrapped, the embedded conn_id is looked up, and the inner frame is sent to the right phone. See pyrycode/pyrycode/docs/protocol-mobile.md § Routing envelope.
Acceptance Criteria
New file internal/relay/forward.go exporting:
func StartPhoneForwarder(ctx context.Context, r *Registry, serverID string, conn Conn, logger *slog.Logger) — starts a goroutine that reads phone frames in a loop. For each frame: wrap with MarshalRoutingEnvelope(conn.ConnID(), frame) (uses relay: routing-envelope wrapper type — marshal, unmarshal, tests #1's helper), look up registry.BinaryFor(serverID), and call binary.Send(wrapped). Errors on Send → log + close phone conn.
func StartBinaryForwarder(ctx context.Context, r *Registry, serverID string, conn Conn, logger *slog.Logger) — starts a goroutine that reads binary frames in a loop. For each frame: UnmarshalRoutingEnvelope(frame), look up the phone in registry.PhonesFor(serverID) matching env.ConnID, call phone.Send(env.Frame). If conn-id not found → log + drop frame (don't close binary; phones come and go).
Both forwarders exit when the context is cancelled OR the source conn returns a Read error.
Phone-to-binary: mock phone+binary conns wired to a registry; phone sends 3 frames; assert binary receives 3 wrapped envelopes with correct conn_id.
Binary-to-phone: mock binary sends an envelope with a known conn_id; assert the matching phone receives the inner frame; assert other phones do NOT receive it.
Multi-phone: 2 phones registered; binary sends envelopes addressed to each; each receives only its own.
Unknown conn_id: binary sends to nonexistent conn_id → log warning, no panic, frame dropped, other connections unaffected.
Phone disconnects mid-stream: forwarder exits cleanly, registry no longer lists the phone.
Binary disconnects: registry releases server-id; in-flight phone forwarders' next Send fails (no binary) → they exit cleanly.
Frames are forwarded as opaque bytes — the relay MUST NOT parse the inner protocol envelope. Tests should explicitly verify a frame containing nested JSON round-trips bytewise.
Backpressure: if a downstream Send blocks, this goroutine blocks. v1 accepts this (small N of phones per server). v2 may add bounded channels per peer; defer until observed.
User Story
As the relay, I want per-connection forwarding goroutines that read frames from each registered phone and binary, wrap them in (or strip from) the routing envelope, and write them to the addressed peer, so that phone-to-binary and binary-to-phone traffic actually flows after the WS upgrades complete.
Context
The actual routing layer. After the WS upgrades (#4, #5) register connections in the registry (#3), this ticket wires the data path: every phone frame is wrapped in the routing envelope (#1) with the phone's
conn_id, and forwarded to the binary; every binary frame is unwrapped, the embeddedconn_idis looked up, and the inner frame is sent to the right phone. Seepyrycode/pyrycode/docs/protocol-mobile.md§ Routing envelope.Acceptance Criteria
internal/relay/forward.goexporting:func StartPhoneForwarder(ctx context.Context, r *Registry, serverID string, conn Conn, logger *slog.Logger)— starts a goroutine that reads phone frames in a loop. For each frame: wrap withMarshalRoutingEnvelope(conn.ConnID(), frame)(uses relay: routing-envelope wrapper type — marshal, unmarshal, tests #1's helper), look upregistry.BinaryFor(serverID), and callbinary.Send(wrapped). Errors on Send → log + close phone conn.func StartBinaryForwarder(ctx context.Context, r *Registry, serverID string, conn Conn, logger *slog.Logger)— starts a goroutine that reads binary frames in a loop. For each frame:UnmarshalRoutingEnvelope(frame), look up the phone inregistry.PhonesFor(serverID)matchingenv.ConnID, callphone.Send(env.Frame). If conn-id not found → log + drop frame (don't close binary; phones come and go).StartPhoneForwarder(orStartBinaryForwarder) on the new conn. The handler's HTTP response goroutine doesn't return until the forwarder exits.internal/relay/forward_test.go:go test -race -count=20clean for the test suite.Technical Notes
Size Estimate
M — two forwarders + integration with handlers + multi-conn test scenarios. ~150 LOC + ~200 LOC tests.
Depends on