Skip to content

feat: subscription lifecycle hardening and readiness accessors#63

Open
amery wants to merge 6 commits into
mainfrom
pr-amery-fixes
Open

feat: subscription lifecycle hardening and readiness accessors#63
amery wants to merge 6 commits into
mainfrom
pr-amery-fixes

Conversation

@amery
Copy link
Copy Markdown
Collaborator

@amery amery commented May 20, 2026

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_SUBSCRIBE until the unsubscribe acknowledgement
    or session end), and the subscribe TYPE_RESPONSE is distinguished
    from the unsubscribe TYPE_RESPONSE that arrives on the same id.
    §6.1 picks up a stateDiagram-v2 of the
    Pending/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_UPDATE that may arrive
    between the unsubscribe request and its acknowledgement.

  • feat(client): surface subscribe ACKs and reject typed nils routes
    the subscribe TYPE_RESPONSE acknowledgement to the typed
    SubscribeCallback as either ErrSubscriptionEstablished on
    STATUS_OK or a ResponseError on any other status, via a
    dispatcher split into isSubscribeACK / subscribeACKErr /
    decodeSubscribePayload. callNewOut rejects typed-nil factory
    results through core.IsNil; GetResponse gains the same
    boundary protection on its client and out arguments.
    ErrSubscriptionEstablished and IsSubscriptionEstablished land
    in the root package.

  • fix(client): demux unsubscribe responses and guard Send fixes
    the dispatcher: popRequestCallback now matches on the
    (request_id, response_type) pair via unsafeIndexCallbacks, so
    the TYPE_RESPONSE acknowledging an Unsubscribe no longer reaches
    the still-registered SubscribeCallback. Send rejects an
    unsubscribe-form TYPE_REQUEST whose target subscription is
    missing or still pending its acknowledgement, factored into
    validateSendArgs / isUnsubscribeShape /
    checkUnsubscribeTarget / normaliseRequestID /
    registerCallback. Failure modes documented on Session.Send
    and the three Client.Unsubscribe* variants.
    popRequestCallback's routing matrix and the Send guard are
    covered with data-driven tests.

Client readiness primitives

  • feat(client): add WaitConnected and connection-state accessors
    exposes Connected(), IsConnected(), and
    WaitConnected(ctx) on Client. Connected() returns a channel
    that closes while the client holds an active session and is
    replaced after each disconnect; IsConnected() is a point-in-time
    snapshot; WaitConnected(ctx) wraps Connected() with the
    caller's context. Closes the doc/code gap left by the
    client/README.md "Connection Management" example, which had
    referenced both accessors before either existed.

Server ergonomic improvements

  • feat(server): accept a handler argument in NewDefaultServer
    allows callers to pass an existing *DefaultMessageHandler — and
    register paths against it — before Server.Serve starts. A nil
    handler preserves the existing behaviour (a fresh
    DefaultMessageHandler with an internal HashCache is built
    in-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 accept
    loop has started taking connections, via an idempotent
    signalReady guarded by mu. The channel stays closed across
    shutdown so late observers never block. The test harness drops
    its time.Sleep(50 * time.Millisecond) heuristic for a
    waitServerReady helper with a 1s ceiling; a new
    TestServer_Ready confirms the open-before-Serve /
    closed-after-loop / stays-closed-across-Shutdown sequence.

Test plan

  • make is green — tidy across all four modules, cspell,
    shellcheck, build.
  • make test is green across generator, nanopb, and
    nanorpc.
  • make race is green; new TestServer_Ready does not flake.
  • Examples in pkg/nanorpc/client/README.md and
    pkg/nanorpc/server/{README.md,doc.go} compile against the
    updated API.

Summary by CodeRabbit

  • Documentation

    • Updated NanoRPC protocol documentation to clarify subscription lifecycle and message routing semantics.
    • Updated client and server README examples for current API usage.
  • New Features

    • Added connection readiness APIs: Connected(), IsConnected(), and WaitConnected() for observing client connection state.
    • Added server readiness signaling via Ready() to wait until the server accepts connections.
  • Tests

    • Added comprehensive test coverage for subscription callbacks, session routing, and connection lifecycle.

Review Change Stack

@amery amery added enhancement New feature or request Review effort 4/5 labels May 20, 2026
@amery amery self-assigned this May 20, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Warning

Rate limit exceeded

@amery has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 37 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c982f8d2-2a19-43be-8971-10a9c78eec5b

📥 Commits

Reviewing files that changed from the base of the PR and between 427295a and 47e3aa2.

📒 Files selected for processing (14)
  • NANORPC_PROTOCOL.md
  • pkg/nanorpc/client/README.md
  • pkg/nanorpc/client/client.go
  • pkg/nanorpc/client/connected_test.go
  • pkg/nanorpc/client/reconnect.go
  • pkg/nanorpc/client/request.go
  • pkg/nanorpc/client/session.go
  • pkg/nanorpc/client/session_test.go
  • pkg/nanorpc/client/subscribe_callback_test.go
  • pkg/nanorpc/errors.go
  • pkg/nanorpc/server/README.md
  • pkg/nanorpc/server/doc.go
  • pkg/nanorpc/server/server.go
  • pkg/nanorpc/server/server_test.go
📝 Walkthrough

Walkthrough

This 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.

Changes

Subscription routing and readiness coordination

Layer / File(s) Summary
Protocol specification for subscription lifecycle
NANORPC_PROTOCOL.md
Request_id is reserved from TYPE_SUBSCRIBE transmission until unsubscribe TYPE_RESPONSE, and updates may arrive during the Unsubscribing phase; protocol now defines four client-side phases (Pending/Active/Unsubscribing/Terminated) with explicit message ordering and delivery guarantees.
Subscription acknowledgement error type
pkg/nanorpc/errors.go
New sentinel ErrSubscriptionEstablished and IsSubscriptionEstablished() helper represent non-failure subscription acknowledgements surfaced via callbacks.
Client connection readiness API and signaling
pkg/nanorpc/client/client.go, pkg/nanorpc/client/reconnect.go, pkg/nanorpc/client/connected_test.go, pkg/nanorpc/client/README.md
Adds Connected(), IsConnected(), and WaitConnected(ctx) APIs with internal connected channel that closes on session attachment and reinitializes on session end, enabling client-side readiness-edge synchronization.
Subscription callback dispatch and error mapping
pkg/nanorpc/client/request.go, pkg/nanorpc/client/subscribe_callback_test.go
Subscribe refactored to distinguish TYPE_RESPONSE (acknowledgement) from TYPE_UPDATE (delivery), allocates fresh outputs per callback via callNewOut, validates non-nil newOut, and maps ack status to either ErrSubscriptionEstablished or ResponseError.
Session queue routing by request type and ID
pkg/nanorpc/client/session.go, pkg/nanorpc/client/session_test.go
clientRequestQueue extended with RequestType and Acknowledged flag; popRequestCallback rewritten to split matches by SUBSCRIBE vs non-SUBSCRIBE entries and route TYPE_UPDATE separately; Send validates unsubscribe targets and normalizes request IDs with dedicated helpers.
Server readiness signaling and NewDefaultServer refactoring
pkg/nanorpc/server/server.go, pkg/nanorpc/server/server_test.go, pkg/nanorpc/server/README.md, pkg/nanorpc/server/doc.go
Adds ready channel to Server with Ready() API that signals from acceptLoop; extends NewDefaultServer to accept optional *DefaultMessageHandler parameter; starts workgroupKeepAlive to prevent premature workgroup drain during shutdown.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • protomcp/nanorpc#34: Adds full unsubscribe protocol implementation on client and server, while this PR refines subscription acknowledgement vs update routing and request_id lifecycle semantics.
  • protomcp/nanorpc#21: Fixes Subscribe to send TYPE_SUBSCRIBE, while this PR refactors subscription ack vs update routing with typed request dispatch.
  • protomcp/nanorpc#22: Introduces modular server architecture with DefaultMessageHandler, which this PR extends by making it an optional constructor parameter.

Poem

🐰 In the land of request_id streams,
Where updates flow through subscription dreams,
We split the ACK from the UPDATE dance,
And signal readiness with channel glance,
So client and server may waltz in sync!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: subscription lifecycle hardening and readiness accessors. It directly reflects the core features added across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-amery-fixes

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Update 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 win

Consider 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 win

Wrap 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 in internal/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 win

Refactor 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 win

Add 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

📥 Commits

Reviewing files that changed from the base of the PR and between 27b5f8a and 427295a.

📒 Files selected for processing (14)
  • NANORPC_PROTOCOL.md
  • pkg/nanorpc/client/README.md
  • pkg/nanorpc/client/client.go
  • pkg/nanorpc/client/connected_test.go
  • pkg/nanorpc/client/reconnect.go
  • pkg/nanorpc/client/request.go
  • pkg/nanorpc/client/session.go
  • pkg/nanorpc/client/session_test.go
  • pkg/nanorpc/client/subscribe_callback_test.go
  • pkg/nanorpc/errors.go
  • pkg/nanorpc/server/README.md
  • pkg/nanorpc/server/doc.go
  • pkg/nanorpc/server/server.go
  • pkg/nanorpc/server/server_test.go

Comment on lines +130 to +134
if subIdx < 0 {
return nil
}
cs.cb[subIdx].Acknowledged = true
return cs.cb[subIdx].Callback
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

❌ Patch coverage is 63.52201% with 58 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/nanorpc/client/session.go 59.72% 28 Missing and 1 partial ⚠️
pkg/nanorpc/client/request.go 52.00% 23 Missing and 1 partial ⚠️
pkg/nanorpc/server/server.go 83.33% 2 Missing and 1 partial ⚠️
pkg/nanorpc/errors.go 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

amery added 6 commits May 20, 2026 16:09
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>
@amery amery force-pushed the pr-amery-fixes branch from 427295a to 47e3aa2 Compare May 20, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Review effort 4/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant