feat: subscription lifecycle hardening and readiness accessors#63
feat: subscription lifecycle hardening and readiness accessors#63amery wants to merge 6 commits into
Conversation
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughThis PR clarifies NanoRPC subscription lifecycle semantics and implements corresponding client and server infrastructure. It introduces connection-readiness signaling for client-server coordination, refactors subscription callback dispatch to distinguish acknowledgements from updates, and extends the server with readiness APIs. The protocol documentation, error types, and queue routing logic align to ensure correct request_id-based routing without misrouting between subscription establishment and update delivery phases. ChangesSubscription routing and readiness coordination
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/nanorpc/server/README.md (1)
107-112:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the DI example to the new server constructor signatures
The “Custom Server with Dependency Injection” snippet still uses outdated call forms (
NewDefaultMessageHandler(),NewDefaultSessionManager(handler),NewServer(listener, sessionManager, handler)). This example should be updated to match current APIs so readers can copy-paste successfully.🤖 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 `@pkg/nanorpc/server/README.md` around lines 107 - 112, The DI example uses outdated constructors (NewDefaultMessageHandler, NewDefaultSessionManager, NewServer); update the snippet to call the current constructor APIs used in the package (replace NewDefaultMessageHandler and NewDefaultSessionManager(handler) with the package's current message handler and session manager constructors and update the NewServer call to the new server constructor signature), ensuring the new function names and required parameters/options match the current server package exports so the example compiles and can be copy-pasted.
🧹 Nitpick comments (4)
pkg/nanorpc/client/connected_test.go (1)
25-123: ⚡ Quick winConsider converting these readiness tests to table-driven form.
The scenarios are related and would align better with repository test style (and be easier to extend) as a table-driven suite.
As per coding guidelines, "
**/*_test.go: Use table-driven tests for comprehensive test coverage in Go."🤖 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 `@pkg/nanorpc/client/connected_test.go` around lines 25 - 123, Convert the multiple readiness tests into a single table-driven test that iterates over scenarios (initially disconnected, opens then closes, swaps after endSession, already connected fast-path, blocks then succeeds, context cancellation) to match repo style; create a test table with a name, setup steps (using newClientForTest, optional calls to setSession or endSession), expected behavior (IsConnected value, whether Connected channel is closed, WaitConnected result or blocking), and any timeouts, then implement a loop that runs each case as t.Run(name, func(t *testing.T){...}) using the existing helpers (Connected, setSession, endSession, WaitConnected) and assertions, preserving the current assertions and timeouts for each scenario.NANORPC_PROTOCOL.md (1)
283-289: ⚡ Quick winWrap the new prose lines to the 80-column markdownlint limit.
Multiple added lines exceed 80 chars and will trip the markdownlint configuration.
As per coding guidelines, "
**/*.md: Markdown files should follow markdownlint rules with 80-character line limits as configured ininternal/build/markdownlint.json."Also applies to: 323-330, 345-346
🤖 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 `@NANORPC_PROTOCOL.md` around lines 283 - 289, The added paragraph about subscription lifetimes and routing exceeds the 80-column markdownlint limit; reflow the prose in NANORPC_PROTOCOL.md so each line is ≤80 chars while preserving wording about TYPE_SUBSCRIBE, TYPE_RESPONSE, TYPE_REQUEST and request_id semantics (first TYPE_RESPONSE = subscription ack, later TYPE_RESPONSE after TYPE_REQUEST with empty data = unsubscribe ack). Apply the same 80-column wrapping to the other affected blocks noted (around the additions at the ranges corresponding to lines 323-330 and 345-346) so the file conforms to the project's markdownlint configuration.pkg/nanorpc/client/subscribe_callback_test.go (1)
41-133: ⚡ Quick winRefactor these callback-path tests into a table-driven suite.
The cases are tightly related and fit naturally into a table-driven matrix (ACK OK, ACK error, update data/no-data/bad-data), which also matches project testing conventions.
As per coding guidelines, "
**/*_test.go: Use table-driven tests for comprehensive test coverage in Go."🤖 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 `@pkg/nanorpc/client/subscribe_callback_test.go` around lines 41 - 133, Combine the individual TestSubscribeCallback_* tests into a single table-driven test (e.g., TestSubscribeCallback_Table) that iterates over cases describing inputs and expected outcomes; each case should provide a name, the nanorpc.NanoRPCResponse (use the existing patterns: TYPE_RESPONSE with STATUS_OK, TYPE_RESPONSE with non-OK, TYPE_UPDATE with data, TYPE_UPDATE without data, TYPE_UPDATE with bad data), the expected error predicate (IsSubscriptionEstablished, IsNotFound, IsNoResponse, nil, or generic decode error) and expected out assertions, and call invokeSubscribeCallback for each row using t.Run; preserve existing assertions (core.AssertNoError, core.AssertErrorIs, core.AssertTrue/False, core.AssertNotNil, core.AssertEqual) but execute them per case so behavior for invokeSubscribeCallback, nanorpc.NanoRPCResponse, and the various status checks remains identical while removing the separate TestSubscribeCallback_ACKSurfacesEstablished, TestSubscribeCallback_ACKErrorStatusSurfacesRealError, TestSubscribeCallback_UpdateWithDataIsDelivered, TestSubscribeCallback_UpdateWithoutDataIsErrNoResponse, and TestSubscribeCallback_UpdateWithBadDataSurfacesDecodeError functions.pkg/nanorpc/client/session_test.go (1)
89-116: ⚡ Quick winAdd a regression row for “SUBSCRIBE-only + non-RESPONSE/non-UPDATE”
Please add one case where a SUBSCRIBE-only queue receives
TYPE_PONG(or unknown type) and assert no callback plus unchanged residue. This will lock in the demux contract and prevent accidental re-ack behavior regressions.🤖 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 `@pkg/nanorpc/client/session_test.go` around lines 89 - 116, Add a regression row in routingTestCases that covers a SUBSCRIBE-only queue receiving a non-RESPONSE/non-UPDATE (e.g. TYPE_PONG): call newRoutingTestCase with name like "subscribe_only_nonresponse_nonupdate", initial state core.S(sub(9)), id 9, response respPong, expected callback index -1, and expected residue core.S(sub(9)); place this new test alongside the other routingTestCase entries to ensure newRoutingTestCase, routingTestCases, core.S, sub, and respPong are exercised and the subscribe-only residue remains unchanged.
🤖 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 `@pkg/nanorpc/client/session.go`:
- Around line 130-134: When matching a SUBSCRIBE callback (subIdx) you currently
set cs.cb[subIdx].Acknowledged = true for any response that isn't TYPE_UPDATE,
which can erroneously acknowledge on other types (e.g., TYPE_PONG); change the
logic so you only set Acknowledged and return cs.cb[subIdx].Callback when the
incoming message's Type equals TYPE_RESPONSE—otherwise do not acknowledge and
return nil. Ensure you reference and check the message type constant
(TYPE_RESPONSE) before modifying cs.cb[subIdx].Acknowledged or returning
cs.cb[subIdx].Callback so only true response messages activate the subscription.
---
Outside diff comments:
In `@pkg/nanorpc/server/README.md`:
- Around line 107-112: The DI example uses outdated constructors
(NewDefaultMessageHandler, NewDefaultSessionManager, NewServer); update the
snippet to call the current constructor APIs used in the package (replace
NewDefaultMessageHandler and NewDefaultSessionManager(handler) with the
package's current message handler and session manager constructors and update
the NewServer call to the new server constructor signature), ensuring the new
function names and required parameters/options match the current server package
exports so the example compiles and can be copy-pasted.
---
Nitpick comments:
In `@NANORPC_PROTOCOL.md`:
- Around line 283-289: The added paragraph about subscription lifetimes and
routing exceeds the 80-column markdownlint limit; reflow the prose in
NANORPC_PROTOCOL.md so each line is ≤80 chars while preserving wording about
TYPE_SUBSCRIBE, TYPE_RESPONSE, TYPE_REQUEST and request_id semantics (first
TYPE_RESPONSE = subscription ack, later TYPE_RESPONSE after TYPE_REQUEST with
empty data = unsubscribe ack). Apply the same 80-column wrapping to the other
affected blocks noted (around the additions at the ranges corresponding to lines
323-330 and 345-346) so the file conforms to the project's markdownlint
configuration.
In `@pkg/nanorpc/client/connected_test.go`:
- Around line 25-123: Convert the multiple readiness tests into a single
table-driven test that iterates over scenarios (initially disconnected, opens
then closes, swaps after endSession, already connected fast-path, blocks then
succeeds, context cancellation) to match repo style; create a test table with a
name, setup steps (using newClientForTest, optional calls to setSession or
endSession), expected behavior (IsConnected value, whether Connected channel is
closed, WaitConnected result or blocking), and any timeouts, then implement a
loop that runs each case as t.Run(name, func(t *testing.T){...}) using the
existing helpers (Connected, setSession, endSession, WaitConnected) and
assertions, preserving the current assertions and timeouts for each scenario.
In `@pkg/nanorpc/client/session_test.go`:
- Around line 89-116: Add a regression row in routingTestCases that covers a
SUBSCRIBE-only queue receiving a non-RESPONSE/non-UPDATE (e.g. TYPE_PONG): call
newRoutingTestCase with name like "subscribe_only_nonresponse_nonupdate",
initial state core.S(sub(9)), id 9, response respPong, expected callback index
-1, and expected residue core.S(sub(9)); place this new test alongside the other
routingTestCase entries to ensure newRoutingTestCase, routingTestCases, core.S,
sub, and respPong are exercised and the subscribe-only residue remains
unchanged.
In `@pkg/nanorpc/client/subscribe_callback_test.go`:
- Around line 41-133: Combine the individual TestSubscribeCallback_* tests into
a single table-driven test (e.g., TestSubscribeCallback_Table) that iterates
over cases describing inputs and expected outcomes; each case should provide a
name, the nanorpc.NanoRPCResponse (use the existing patterns: TYPE_RESPONSE with
STATUS_OK, TYPE_RESPONSE with non-OK, TYPE_UPDATE with data, TYPE_UPDATE without
data, TYPE_UPDATE with bad data), the expected error predicate
(IsSubscriptionEstablished, IsNotFound, IsNoResponse, nil, or generic decode
error) and expected out assertions, and call invokeSubscribeCallback for each
row using t.Run; preserve existing assertions (core.AssertNoError,
core.AssertErrorIs, core.AssertTrue/False, core.AssertNotNil, core.AssertEqual)
but execute them per case so behavior for invokeSubscribeCallback,
nanorpc.NanoRPCResponse, and the various status checks remains identical while
removing the separate TestSubscribeCallback_ACKSurfacesEstablished,
TestSubscribeCallback_ACKErrorStatusSurfacesRealError,
TestSubscribeCallback_UpdateWithDataIsDelivered,
TestSubscribeCallback_UpdateWithoutDataIsErrNoResponse, and
TestSubscribeCallback_UpdateWithBadDataSurfacesDecodeError functions.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro
Run ID: c8cf6dae-54c2-406c-946b-e7d60f3984d2
📒 Files selected for processing (14)
NANORPC_PROTOCOL.mdpkg/nanorpc/client/README.mdpkg/nanorpc/client/client.gopkg/nanorpc/client/connected_test.gopkg/nanorpc/client/reconnect.gopkg/nanorpc/client/request.gopkg/nanorpc/client/session.gopkg/nanorpc/client/session_test.gopkg/nanorpc/client/subscribe_callback_test.gopkg/nanorpc/errors.gopkg/nanorpc/server/README.mdpkg/nanorpc/server/doc.gopkg/nanorpc/server/server.gopkg/nanorpc/server/server_test.go
| if subIdx < 0 { | ||
| return nil | ||
| } | ||
| cs.cb[subIdx].Acknowledged = true | ||
| return cs.cb[subIdx].Callback |
There was a problem hiding this comment.
Acknowledge subscriptions only on TYPE_RESPONSE
Acknowledged is currently set for any non-TYPE_UPDATE response when only a SUBSCRIBE entry matches. That can incorrectly activate a subscription on unexpected response types (for example TYPE_PONG with a colliding request_id).
Suggested fix
- if subIdx < 0 {
+ if subIdx < 0 {
return nil
}
+ if respType != nanorpc.NanoRPCResponse_TYPE_RESPONSE {
+ return nil
+ }
cs.cb[subIdx].Acknowledged = true
return cs.cb[subIdx].Callback📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if subIdx < 0 { | |
| return nil | |
| } | |
| cs.cb[subIdx].Acknowledged = true | |
| return cs.cb[subIdx].Callback | |
| if subIdx < 0 { | |
| return nil | |
| } | |
| if respType != nanorpc.NanoRPCResponse_TYPE_RESPONSE { | |
| return nil | |
| } | |
| cs.cb[subIdx].Acknowledged = true | |
| return cs.cb[subIdx].Callback |
🤖 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 `@pkg/nanorpc/client/session.go` around lines 130 - 134, When matching a
SUBSCRIBE callback (subIdx) you currently set cs.cb[subIdx].Acknowledged = true
for any response that isn't TYPE_UPDATE, which can erroneously acknowledge on
other types (e.g., TYPE_PONG); change the logic so you only set Acknowledged and
return cs.cb[subIdx].Callback when the incoming message's Type equals
TYPE_RESPONSE—otherwise do not acknowledge and return nil. Ensure you reference
and check the message type constant (TYPE_RESPONSE) before modifying
cs.cb[subIdx].Acknowledged or returning cs.cb[subIdx].Callback so only true
response messages activate the subscription.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
The subscribe and unsubscribe acknowledgements share a single request_id, and the original wording in §5.4 and §6.1 left two gaps: it did not say how long the request_id is reserved, and it did not distinguish the subscribe TYPE_RESPONSE from the unsubscribe TYPE_RESPONSE that arrives on the same id. §5.4 now states the reservation window explicitly — from TYPE_SUBSCRIBE until the unsubscribe acknowledgement or session end — and warns that routing keyed only on request_id will misroute one of the two TYPE_RESPONSEs. §6.1 expands the termination bullets, adds a stateDiagram-v2 of the Pending/Active/Unsubscribing/Terminated transitions, and introduces a Phases subsection naming each phase and which message types may arrive in it. §6.3 adds a Termination bullet covering the in-flight TYPE_UPDATE that may still arrive between the unsubscribe request and its acknowledgement. §10.1's ASCII sequence shows the same in-flight TYPE_UPDATE alongside the unsubscribe acknowledgement. §2.2's request-response ASCII diagram picks up a one-space column alignment fix. Signed-off-by: Alejandro Mery <amery@apptly.co>
Expose three readiness primitives on the [Client]: - Connected() returns a channel that closes whilst the client holds an active session. The channel is replaced after each disconnect, so it signals the next readiness edge and callers waiting across a reconnect cycle must re-fetch. - IsConnected() reports the current session state as a point-in-time snapshot. - WaitConnected(ctx) wraps Connected with the caller's context, so consumers can ride out a brief reconnect blip without polling Pong(). Closes the doc/code gap left by the README's "Connection Management" example, which had referenced Connected() and IsConnected() before either existed. The readiness channel is initialised in (*Config).New, closed inside setSession after the session pointer is attached, and swapped for a fresh open channel inside endSession. Both transitions happen under the existing mu guarding c.cs. Signed-off-by: Alejandro Mery <amery@apptly.co>
Allow callers to pass an existing *DefaultMessageHandler — and so register paths against it — before [Server.Serve] starts. When the handler is nil, the existing behaviour is preserved: a fresh DefaultMessageHandler with an internal HashCache is built in-place. Update the doc.go and README examples so the call shape matches the new signature, and add an integration test that registers an echo handler, drives a real TCP request through the server, and verifies the payload round-trips. Signed-off-by: Alejandro Mery <amery@apptly.co>
Add a `ready chan struct{}` field to [Server] and a public
`Ready() <-chan struct{}` accessor that closes once the accept loop
has started taking connections. The close is performed by an
idempotent `signalReady` helper guarded by `mu`, so repeated Serve
calls do not panic on a double close, and the channel stays closed
across shutdown so late observers never block.
Switch the test harness off the `time.Sleep(50 * time.Millisecond)`
heuristic: a `waitServerReady` helper now blocks on `Ready()` with a
generous 1s ceiling, and a new `TestServer_Ready` confirms the
channel is open before Serve, closes after the loop is reached, and
stays closed across Shutdown.
Signed-off-by: Alejandro Mery <amery@apptly.co>
The server's TYPE_RESPONSE acknowledgement now reaches the typed SubscribeCallback as either ErrSubscriptionEstablished on STATUS_OK or a ResponseError on any other status, via a dispatcher split into isSubscribeACK, subscribeACKErr and decodeSubscribePayload, plus a callNewOut helper that rejects typed-nil factory results through core.IsNil. The newOut factory is now required at the public Subscribe boundary. GetResponse gains the same boundary protection: typed-nil client and out arguments are rejected with core.ErrInvalid, and the wait arm is extracted into waitGetResponse so the function stays under the cognitive-complexity floor. Add ErrSubscriptionEstablished and IsSubscriptionEstablished in the root package so callers can recognise the acknowledgement. Signed-off-by: Alejandro Mery <amery@apptly.co>
The dispatcher previously keyed callbacks on request_id alone, so the TYPE_RESPONSE acknowledging an Unsubscribe — which reuses the subscription's request_id — reached the still-registered SubscribeCallback. With subscribe-ACK surfacing in place, this showed up as ErrSubscriptionEstablished firing after the caller had already unsubscribed. popRequestCallback now matches on the (request_id, response_type) pair via unsafeIndexCallbacks: TYPE_UPDATE routes to the SUBSCRIBE entry without removing it; any other response prefers the non-SUBSCRIBE entry and drops both entries when a SUBSCRIBE entry shadowed it; a TYPE_RESPONSE that resolves only a SUBSCRIBE entry is the subscribe acknowledgement, flips a new Acknowledged bit on the entry, and keeps the entry queued for subsequent updates. Send mirrors the protocol shape by rejecting an unsubscribe-form TYPE_REQUEST (positive request_id on a non-subscribe) whose target subscription is missing or still pending its acknowledgement. The function is factored into validateSendArgs, isUnsubscribeShape, checkUnsubscribeTarget, normaliseRequestID and registerCallback so the pipeline reads in order. Document the new os.ErrInvalid failure modes on Session.Send and on Client.Unsubscribe, Client.UnsubscribeByHash and Client.UnsubscribeWithHash, each cross-linked to its matching Subscribe variant. Cover popRequestCallback's routing matrix and the Send guard with data-driven tests under package client. Signed-off-by: Alejandro Mery <amery@apptly.co>
Summary
Subscription lifecycle correctness across the client and server,
pinned by a protocol-document clarification that lands first so the
code can align with the new wording. Adjacent client and server
readiness primitives close doc/code gaps that the README had
referenced ahead of implementation.
Subscription lifecycle
Three commits address the request_id-sharing contract end-to-end.
docs(protocol): clarify subscription request_id lifetime and demux
closes two gaps in NANORPC_PROTOCOL.md §5.4 and §6.1: the
reservation window for a subscription's request_id is named
explicitly (
TYPE_SUBSCRIBEuntil the unsubscribe acknowledgementor session end), and the subscribe
TYPE_RESPONSEis distinguishedfrom the unsubscribe
TYPE_RESPONSEthat arrives on the same id.§6.1 picks up a
stateDiagram-v2of thePending/Active/Unsubscribing/Terminated transitions and a Phases
subsection naming which message types may arrive in each phase.
§6.3 and §10.1 cover the in-flight
TYPE_UPDATEthat may arrivebetween the unsubscribe request and its acknowledgement.
feat(client): surface subscribe ACKs and reject typed nils routes
the subscribe
TYPE_RESPONSEacknowledgement to the typedSubscribeCallbackas eitherErrSubscriptionEstablishedonSTATUS_OKor aResponseErroron any other status, via adispatcher split into
isSubscribeACK/subscribeACKErr/decodeSubscribePayload.callNewOutrejects typed-nil factoryresults through
core.IsNil;GetResponsegains the sameboundary protection on its client and out arguments.
ErrSubscriptionEstablishedandIsSubscriptionEstablishedlandin the root package.
fix(client): demux unsubscribe responses and guard Send fixes
the dispatcher:
popRequestCallbacknow matches on the(request_id, response_type)pair viaunsafeIndexCallbacks, sothe
TYPE_RESPONSEacknowledging an Unsubscribe no longer reachesthe still-registered
SubscribeCallback.Sendrejects anunsubscribe-form
TYPE_REQUESTwhose target subscription ismissing or still pending its acknowledgement, factored into
validateSendArgs/isUnsubscribeShape/checkUnsubscribeTarget/normaliseRequestID/registerCallback. Failure modes documented onSession.Sendand the three
Client.Unsubscribe*variants.popRequestCallback's routing matrix and theSendguard arecovered with data-driven tests.
Client readiness primitives
exposes
Connected(),IsConnected(), andWaitConnected(ctx)onClient.Connected()returns a channelthat closes while the client holds an active session and is
replaced after each disconnect;
IsConnected()is a point-in-timesnapshot;
WaitConnected(ctx)wrapsConnected()with thecaller's context. Closes the doc/code gap left by the
client/README.md"Connection Management" example, which hadreferenced both accessors before either existed.
Server ergonomic improvements
feat(server): accept a handler argument in NewDefaultServer
allows callers to pass an existing
*DefaultMessageHandler— andregister paths against it — before
Server.Servestarts. A nilhandler preserves the existing behaviour (a fresh
DefaultMessageHandlerwith an internalHashCacheis builtin-place). Integration test drives an echo handler through a real
TCP request.
feat(server): expose a Ready channel for the accept loop adds a
Ready() <-chan struct{}accessor that closes once the acceptloop has started taking connections, via an idempotent
signalReadyguarded bymu. The channel stays closed acrossshutdown so late observers never block. The test harness drops
its
time.Sleep(50 * time.Millisecond)heuristic for awaitServerReadyhelper with a 1s ceiling; a newTestServer_Readyconfirms the open-before-Serve /closed-after-loop / stays-closed-across-Shutdown sequence.
Test plan
makeis green — tidy across all four modules, cspell,shellcheck, build.
make testis green acrossgenerator,nanopb, andnanorpc.make raceis green; newTestServer_Readydoes not flake.pkg/nanorpc/client/README.mdandpkg/nanorpc/server/{README.md,doc.go}compile against theupdated API.
Summary by CodeRabbit
Documentation
New Features
Connected(),IsConnected(), andWaitConnected()for observing client connection state.Ready()to wait until the server accepts connections.Tests