Skip to content

Add favorite.toggle RPC for per-user room favorites#247

Merged
general-lex merged 1 commit into
mainfrom
claude/focused-hypatia-gijQ6
Jun 3, 2026
Merged

Add favorite.toggle RPC for per-user room favorites#247
general-lex merged 1 commit into
mainfrom
claude/focused-hypatia-gijQ6

Conversation

@general-lex
Copy link
Copy Markdown
Collaborator

@general-lex general-lex commented Jun 1, 2026

Summary

Implements a new client-facing NATS RPC favorite.toggle that flips a boolean Subscription.favorite flag for the requester on a single room. The flag is a per-user, per-room render hint for a "favorites" sidebar section; the backend treats it as opaque and does not gate notifications, routing, or validation.

Architecturally mirrors the existing mute.toggle RPC (PR #217): same subject shape, same store pattern, same cross-site replication via Outbox/Inbox streams. The mute and favorite flags are independent toggles on Subscription with parallel, non-coupled handlers.

Key Changes

Model & Events

  • Add Favorite bool field to Subscription (always serialized, no omitempty)
  • Add FavoriteToggleResponse with Status and Favorite fields
  • Add SubscriptionFavoriteToggledEvent for cross-site federation payloads
  • Add OutboxSubscriptionFavoriteToggled event-type constant
  • Update SubscriptionUpdateEvent.Action enum to include "favorite_toggled"

Subject Builders (pkg/subject/subject.go)

  • FavoriteToggle(account, roomID, siteID) → concrete subject
  • FavoriteToggleWildcard(siteID) → wildcard for queue subscription

Room Service Handler (room-service/handler.go)

  • Register queue subscriber on FavoriteToggleWildcard
  • natsFavoriteToggle wraps context and error handling
  • handleFavoriteToggle orchestrates the toggle:
    1. Parse subject via ParseUserRoomSubject
    2. Call store.ToggleSubscriptionFavorite (atomic Mongo FindOneAndUpdate)
    3. Publish SubscriptionUpdateEvent with Action: "favorite_toggled" (non-fatal on failure)
    4. If cross-site (user's home site ≠ room's site), publish SubscriptionFavoriteToggledEvent to Outbox stream
    5. Reply with {status: "ok", favorite: <post-flip value>}

Store Interface & Implementation (room-service/store.go, store_mongo.go)

  • ToggleSubscriptionFavorite(ctx, roomID, account) atomically flips the bit via $not and $ifNull (handles legacy docs without the field)
  • No new indexes required (reuses existing (roomId, "u.account") compound index)

Inbox Worker (inbox-worker/handler.go, main.go)

  • Add dispatch case for "subscription_favorite_toggled" events
  • UpdateSubscriptionFavorite store method sets the flag by (roomID, account)
  • Missing-subscription is a silent no-op (federation race — user may have left the room)

Client API Documentation (docs/client-api.md)

  • New "Toggle Favorite" RPC section documenting subject, request/response, error cases, and cross-site behavior
  • Update "Subscription Update" event documentation to include "favorite_toggled" action and favorite field

Implementation Details

  • Idempotency: This is a toggle, not a set. Every successful call flips the bit. Clients must debounce; the handler does not deduplicate (same as mute.toggle).
  • Legacy-safe: The Mongo pipeline uses $ifNull: ["$favorite", false] so documents written before this change (no favorite key) deterministically toggle to true on first call. No migration script required.
  • Non-fatal core publish: If the SubscriptionUpdateEvent publish fails on same-site toggles, the handler still replies success — the DB write is the source of truth and clients reconcile on next refetch.
  • Fatal cross-site publish: If the Outbox publish fails on cross-site toggles, the client gets an error and federation will retry on the user's next action.
  • No behavioral coupling: Favorites do not affect notifications, broadcast routing, or message validation

https://claude.ai/code/session_01CNQTuBnbtUqQcoNKcF3i8f

Summary by CodeRabbit

  • New Features

    • Added subscription favorite toggle functionality through a new RPC endpoint.
  • Documentation

    • Updated API reference with complete Toggle Favorite endpoint documentation and specifications.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Warning

Review limit reached

@general-lex, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 3 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dfd6d825-155d-4f6a-bdfa-34f9bdfa725c

📥 Commits

Reviewing files that changed from the base of the PR and between 0e6efba and 9c54abf.

📒 Files selected for processing (16)
  • docs/client-api.md
  • docs/superpowers/specs/2026-06-01-favorite-toggle-rpc-design.md
  • inbox-worker/handler.go
  • inbox-worker/handler_test.go
  • inbox-worker/main.go
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/model/subscription.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-service/integration_test.go
  • room-service/mock_store_test.go
  • room-service/store.go
  • room-service/store_mongo.go
📝 Walkthrough

Walkthrough

This PR implements a subscription favorite-toggle RPC feature across room-service and inbox-worker, allowing users to synchronously flip a per-room favorite boolean with automatic cross-site federation support.

Changes

Favorite Toggle RPC Implementation

Layer / File(s) Summary
Data Model & Event Types
pkg/model/subscription.go, pkg/model/event.go, pkg/model/model_test.go
Add Favorite field to Subscription; define FavoriteToggleResponse and SubscriptionFavoriteToggledEvent types; add OutboxSubscriptionFavoriteToggled constant; include JSON round-trip contract tests for new types.
Subject Builders & Parsing
pkg/subject/subject.go, pkg/subject/subject_test.go
Introduce FavoriteToggle(account, roomID, siteID) and FavoriteToggleWildcard(siteID) builders; add tests covering concrete subject formatting, wildcard patterns, and round-trip parsing.
Room-Service Store Contract & Implementation
room-service/store.go, room-service/store_mongo.go, room-service/integration_test.go, room-service/mock_store_test.go
Define ToggleSubscriptionFavorite interface method; implement atomic MongoDB toggle using $ifNull + $not pipeline to support legacy documents; add integration test covering legacy-doc toggling and missing-subscription behavior; generate mocks.
Room-Service Request Handler
room-service/handler.go, room-service/handler_test.go
Register NATS queue subscription for favorite-toggle requests; implement handler that parses subject, toggles state via store, publishes core subscription.update event (non-fatal), publishes cross-site outbox event (fatal), and responds with updated favorite flag; add comprehensive unit test coverage for success, cross-site federation, error cases, and publish-failure semantics.
Client-Safe Error Messaging
room-service/helper.go, room-service/helper_test.go
Whitelist "invalid favorite-toggle subject" in error sanitization allowlist; add test case for error message passthrough.
Inbox-Worker Cross-Site Federation
inbox-worker/handler.go, inbox-worker/main.go, inbox-worker/handler_test.go
Extend InboxStore with UpdateSubscriptionFavorite method documented as silent no-op on missing subscription; wire "subscription_favorite_toggled" event type to new handler; implement MongoDB update of home-site subscription; add tests covering successful update, missing-subscription no-op, and malformed-payload error handling.
Design Specification & Client API Documentation
docs/superpowers/specs/2026-06-01-favorite-toggle-rpc-design.md, docs/client-api.md
Document complete RPC design including subject patterns, wire format, handler flow, store behavior, legacy-document handling, cross-site federation semantics, error routing, behavioral rules (render-only, no notification gating), and testing strategy; update client API reference with Toggle Favorite RPC section and subscription.update event schema documentation.

Sequence Diagram

sequenceDiagram
  participant Client
  participant RoomService
  participant RoomMongo as Room Mongo Store
  participant CoreStream
  participant OutboxStream
  participant InboxWorker
  participant HomeMongo as Home Mongo Store
  
  Client->>RoomService: favorite.toggle request<br/>(account, roomID)
  RoomService->>RoomMongo: ToggleSubscriptionFavorite<br/>(roomID, account)
  RoomMongo-->>RoomService: *Subscription{Favorite}
  RoomService->>CoreStream: publish SubscriptionUpdate<br/>(action: favorite_toggled)
  RoomService->>OutboxStream: publish OutboxSubscriptionFavoriteToggled<br/>(if cross-site)
  RoomService-->>Client: FavoriteToggleResponse{Favorite}
  
  OutboxStream->>InboxWorker: SubscriptionFavoriteToggled event
  InboxWorker->>HomeMongo: UpdateSubscriptionFavorite<br/>(roomID, account, favorite)
  HomeMongo-->>InboxWorker: nil or silent no-op
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • hmchangw/chat#72: Both PRs modify inbox-worker/handler.go by extending InboxStore and adding new HandleEvent cases to update subscription state—main adds "subscription_favorite_toggled" handling, while the retrieved PR adds "role_updated" handling.

Suggested reviewers

  • mliu33
  • vjauhari-work

Poem

🐰 A toggle hops from room to home,
Favorite flags in binary roam,
Cross-site mirrors dance and play,
While tests ensure they're here to stay! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing a new favorite.toggle RPC that allows per-user room favorites, which directly aligns with the comprehensive feature implementation across model, handler, store, inbox, and documentation layers.
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 claude/focused-hypatia-gijQ6

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: 3

🧹 Nitpick comments (1)
pkg/subject/subject_test.go (1)

366-388: ⚡ Quick win

Add invalid-account coverage for FavoriteToggle.

Please add a negative test (wildcard account * / >) to lock in the account-token guard behavior, similar to other subject builder guard tests in this file.

🤖 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/subject/subject_test.go` around lines 366 - 388, Add a negative test that
ensures wildcard account tokens are rejected: call subject.FavoriteToggle with
account "*" (and another case with account ">") and a valid room ("r1") and site
("site-a"), then pass the resulting subject into subject.ParseUserRoomSubject
and assert that ok is false (and account/roomID are not treated as valid
"alice"/"r1"). This mirrors other subject guard tests and locks in the
account-token guard behavior for FavoriteToggle and ParseUserRoomSubject.
🤖 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/subject/subject.go`:
- Around line 379-381: FavoriteToggle currently interpolates account directly
and can leak wildcard tokens; add the same shared account-token guard used
elsewhere by calling isValidAccountToken(account) at the top of FavoriteToggle
and if it returns false bail out (e.g., return ""), otherwise proceed to
fmt.Sprintf as before; reference the FavoriteToggle function and
isValidAccountToken validator when making the change.

In `@room-service/handler.go`:
- Around line 1506-1559: The toggle currently commits via
h.store.ToggleSubscriptionFavorite then performs federation fanout
(GetUserSiteID, publishToStream) and returns an error if fanout fails, which can
re-trigger the toggle on retry; change to an acknowledged-after-commit pattern:
after ToggleSubscriptionFavorite succeeds, ensure outbox events are durably
recorded (e.g., insert an OutboxEvent row via a new store method like
CreateOutboxEvent or use an existing outbox writer) before attempting
publishToStream, and never return a retriable error to the client if outbox
publish fails; instead log the publish error and rely on a background worker to
read pending OutboxEvent entries and call publishToStream (or retry publish)
until success. Reference ToggleSubscriptionFavorite, GetUserSiteID,
publishToStream, model.OutboxEvent and the favorite-toggled outbox publish paths
when implementing durable outbox insertion and background retry.
- Around line 1480-1490: The error log in natsFavoriteToggle currently prints
m.Msg.Subject (leaking account/room info); change the slog.Error call to avoid
the raw subject and instead log the request ID from the ctx returned by
wrappedCtx and any safe structured fields (e.g., room_id or caller_id if already
parsed by handleFavoriteToggle) — do not include the full NATS subject. Update
natsFavoriteToggle to extract requestID from ctx (the same context used for
handleFavoriteToggle), pass that as "request_id" in slog.Error along with
sanitized fields (or omit sensitive identifiers), and keep calling
natsutil.ReplyError(sanitizeError(err)). Ensure the other respond-error logging
remains unchanged.

---

Nitpick comments:
In `@pkg/subject/subject_test.go`:
- Around line 366-388: Add a negative test that ensures wildcard account tokens
are rejected: call subject.FavoriteToggle with account "*" (and another case
with account ">") and a valid room ("r1") and site ("site-a"), then pass the
resulting subject into subject.ParseUserRoomSubject and assert that ok is false
(and account/roomID are not treated as valid "alice"/"r1"). This mirrors other
subject guard tests and locks in the account-token guard behavior for
FavoriteToggle and ParseUserRoomSubject.
🪄 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: d834a03e-9519-4e29-ac57-d890eb6511f6

📥 Commits

Reviewing files that changed from the base of the PR and between 7769d41 and 0e6efba.

📒 Files selected for processing (18)
  • docs/client-api.md
  • docs/superpowers/specs/2026-06-01-favorite-toggle-rpc-design.md
  • inbox-worker/handler.go
  • inbox-worker/handler_test.go
  • inbox-worker/main.go
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/model/subscription.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-service/helper.go
  • room-service/helper_test.go
  • room-service/integration_test.go
  • room-service/mock_store_test.go
  • room-service/store.go
  • room-service/store_mongo.go

Comment thread pkg/subject/subject.go
Comment on lines +379 to +381
func FavoriteToggle(account, roomID, siteID string) string {
return fmt.Sprintf("chat.user.%s.request.room.%s.%s.favorite.toggle", account, roomID, siteID)
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jun 1, 2026

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

Add the shared account-token guard in FavoriteToggle.

FavoriteToggle accepts account but skips isValidAccountToken, so wildcard tokens (*, >) can leak into constructed subjects. Match the same guard pattern used by other account-based builders.

Suggested patch
 func FavoriteToggle(account, roomID, siteID string) string {
+	if !isValidAccountToken(account) {
+		panic("invalid account token: contains NATS wildcard characters")
+	}
 	return fmt.Sprintf("chat.user.%s.request.room.%s.%s.favorite.toggle", account, roomID, siteID)
 }

Based on learnings: “For Go subject builder/parser code under pkg/subject, add a single shared guard/validator for any function that accepts an account token… reject * and >.”

📝 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
func FavoriteToggle(account, roomID, siteID string) string {
return fmt.Sprintf("chat.user.%s.request.room.%s.%s.favorite.toggle", account, roomID, siteID)
}
func FavoriteToggle(account, roomID, siteID string) string {
if !isValidAccountToken(account) {
panic("invalid account token: contains NATS wildcard characters")
}
return fmt.Sprintf("chat.user.%s.request.room.%s.%s.favorite.toggle", account, roomID, siteID)
}
🤖 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/subject/subject.go` around lines 379 - 381, FavoriteToggle currently
interpolates account directly and can leak wildcard tokens; add the same shared
account-token guard used elsewhere by calling isValidAccountToken(account) at
the top of FavoriteToggle and if it returns false bail out (e.g., return ""),
otherwise proceed to fmt.Sprintf as before; reference the FavoriteToggle
function and isValidAccountToken validator when making the change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verified — the guard is missing across the entire room-request family, not just FavoriteToggle. Two adjacent conventions live side by side in pkg/subject/subject.go:

Guarded (User* family): UserStatus*, UserProfile*, UserSubscription* (all 8), MsgHistory, MsgThread
Unguarded (room-request family): MsgSend, MsgGet, MemberRoleUpdate, MemberRemove, MemberList, RoomCreate, MessageRead, MuteToggle, FavoriteToggle, SubscriptionUpdate

The parser side has the same split: ParseRoomCreateSubject rejects wildcard tokens (line 478); ParseUserRoomSubject and ParseUserRoomSiteSubject (used by every room-request handler) don't. Historical drift since PR #179, not deliberate.

Adding the guard to FavoriteToggle alone makes it the only outlier in its family without touching the real defense-in-depth surface — the unguarded parsers are where malicious inbound subjects would land, not the outbound builders that this PR ships. The right scope is a focused PR that retrofits ParseUserRoomSubject/ParseUserRoomSiteSubject and the room-family builders together. Leaving as-is here for sibling parity; tracking the family-wide cleanup separately.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@general-lex, understood — the split is a family-level pattern, not an individual function gap, and patching only FavoriteToggle would leave it as the lone guarded member of an otherwise unguarded cohort. The real leverage is at ParseUserRoomSubject/ParseUserRoomSiteSubject where inbound subjects land. I'll withdraw the comment and note the design for future reviews.

Would you like me to open a GitHub issue to track the family-wide retrofit of ParseUserRoomSubject, ParseUserRoomSiteSubject, and the room-request builders?


✏️ Learnings added
Learnt from: general-lex
Repo: hmchangw/chat PR: 247
File: pkg/subject/subject.go:379-381
Timestamp: 2026-06-01T08:54:00.378Z
Learning: In hmchangw/chat `pkg/subject/subject.go`, the account-token guard (`isValidAccountToken` / panic on `*` or `>`) is intentionally present only in the **User\* family** of builders (`UserStatus*`, `UserProfile*`, `UserSubscription*` (all 8), `MsgHistory`, `MsgThread`) and absent from the entire **room-request family** (`MsgSend`, `MsgGet`, `MemberRoleUpdate`, `MemberRemove`, `MemberList`, `RoomCreate`, `MessageRead`, `MuteToggle`, `FavoriteToggle`, `SubscriptionUpdate`). This is historical drift since PR `#179`, not deliberate design. The room-request builders are left unguarded for sibling parity pending a family-wide cleanup PR that will retrofit both the builders and the corresponding parsers (`ParseUserRoomSubject`, `ParseUserRoomSiteSubject`). Do NOT flag individual room-request builders for missing the guard; flag only new builders that deviate from their immediate sibling convention.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: mliu33
Repo: hmchangw/chat PR: 55
File: pkg/subject/subject.go:16-20
Timestamp: 2026-04-08T01:06:42.136Z
Learning: For Go subject builder/parser code under pkg/subject, add a single shared guard/validator for any function that accepts an `account` token to prevent NATS subject special-character issues. Specifically, reject account values containing NATS wildcard characters `*` and `>` (use one common validation function so all call sites behave consistently). Rely on the assumption that `.` is already operationally absent only if that guarantee is enforced elsewhere (e.g., by upstream validation/CI); otherwise include `.` in the guard as well.

Learnt from: Joey0538
Repo: hmchangw/chat PR: 59
File: pkg/roomkeysender/roomkeysender.go:28-30
Timestamp: 2026-04-08T08:25:32.108Z
Learning: In this repository’s Go code, follow the CLAUDE.md rule: don’t add defensive error handling, fallbacks, or nil/validation guards for conditions that are guaranteed not to occur. For internal functions/methods (e.g., when all callers are known to pass non-nil pointer parameters, such as pkg/roomkeysender/roomkeysender.go’s Send), the absence of nil-pointer guards is acceptable—do not flag it as a review issue. Reserve pointer validation and other safety checks for system boundaries (e.g., external inputs, API/HTTP requests, deserialization, or other untrusted entry points).

Learnt from: Joey0538
Repo: hmchangw/chat PR: 59
File: pkg/model/model_test.go:306-317
Timestamp: 2026-04-08T08:25:48.132Z
Learning: In Go, `time.Time` is `comparable` and therefore may be used as a type argument to generic constraints requiring `comparable` (e.g., `func roundTrip[T comparable](...)`). This is because `time.Time`’s underlying fields are all comparable (`uint64`, `int64`, and a pointer `*Location`, where pointers are comparable by address). As a result, any struct that contains or embeds a `time.Time` field (e.g., `model.Message`, or structs embedding it like `model.NotificationEvent`) should also satisfy `comparable` and should not be flagged as a compilation-error when used with `T comparable`.

Learnt from: general-lex
Repo: hmchangw/chat PR: 141
File: message-worker/main.go:95-99
Timestamp: 2026-05-04T03:30:38.678Z
Learning: In hmchangw/chat, `pkg/natsutil/connect.go`’s `natsutil.Connect` returns an OpenTelemetry-wrapped NATS connection (`*otelnats.Conn` from `github.com/Marz32onE/instrumentation-go/otel-nats/otelnats`), not a plain `*nats.Conn`. Because `otelnats.Conn.Publish` is defined as `Publish(ctx context.Context, subj string, data []byte) error` (it uses `ctx` to propagate traces via NATS headers), code review should NOT flag `nc.Publish(ctx, subj, data)` as an incorrect method call shape. This `Publish(ctx, ...)` pattern is the standard/correct usage across the codebase (e.g., message-worker, broadcast-worker, notification-worker, history-service).

Learnt from: Joey0538
Repo: hmchangw/chat PR: 182
File: docs/superpowers/specs/2026-05-14-message-edit-delete-canonical-events-design.md:172-172
Timestamp: 2026-05-18T00:31:05.210Z
Learning: In hmchangw/chat, when defining `subject.UserRoomEvent`-style chat event subject strings, use the segment order `chat.user.%s.event.room` (and wildcards `chat.user.*.event.room`) rather than `chat.user.<account>.room.event`. Suffixes under this pattern must extend in the same order, e.g. `.event.room.update`, `.event.room.key`, and `.event.room.metadata.update`. Review new/changed subject definitions to ensure they follow the established `.event.room` ordering convention.

Comment thread room-service/handler.go
Comment on lines +1480 to +1490
func (h *Handler) natsFavoriteToggle(m otelnats.Msg) {
ctx := wrappedCtx(m)
resp, err := h.handleFavoriteToggle(ctx, m.Msg.Subject, m.Msg.Data)
if err != nil {
slog.Error("favorite toggle failed", "error", err, "subject", m.Msg.Subject)
natsutil.ReplyError(m.Msg, sanitizeError(err))
return
}
if err := m.Msg.Respond(resp); err != nil {
slog.Error("failed to respond to favorite toggle", "error", err)
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jun 1, 2026

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

Avoid logging the raw NATS subject here.

m.Msg.Subject encodes the caller account and room ID, so this new error log leaks user identifiers while still omitting the request ID that should correlate the failure. Log the request ID plus safe structured fields instead of the full subject.

🤖 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 `@room-service/handler.go` around lines 1480 - 1490, The error log in
natsFavoriteToggle currently prints m.Msg.Subject (leaking account/room info);
change the slog.Error call to avoid the raw subject and instead log the request
ID from the ctx returned by wrappedCtx and any safe structured fields (e.g.,
room_id or caller_id if already parsed by handleFavoriteToggle) — do not include
the full NATS subject. Update natsFavoriteToggle to extract requestID from ctx
(the same context used for handleFavoriteToggle), pass that as "request_id" in
slog.Error along with sanitized fields (or omit sensitive identifiers), and keep
calling natsutil.ReplyError(sanitizeError(err)). Ensure the other respond-error
logging remains unchanged.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verified against the existing code — this is the established pattern across room-service/handler.go:

  • :119 slog.Error("create-room failed", "error", err, "subject", m.Msg.Subject)
  • :1399 slog.Error("mute toggle failed", "error", err, "subject", m.Msg.Subject)
  • :1484 slog.Error("favorite toggle failed", "error", err, "subject", m.Msg.Subject) ← this PR

The PII concern is valid in principle but applies uniformly to the whole room-service handler family, not just the favorite-toggle path. Fixing it here alone would split log shape across siblings (operators grepping for failures would get two formats from the same service) without addressing the underlying concern.

The right scope is a service-wide sweep — replace subject with request_id + safe structured fields across every handler in one focused PR. Leaving as-is here to preserve parity with the mute-toggle pattern this PR explicitly mirrors; tracking the broader cleanup as a separate follow-up.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@general-lex, that's a sound call — splitting log shape mid-file would hurt operability more than it helps in isolation. The consistency argument is correct, and a single focused sweep is the right vehicle for this.

Would you like me to open a follow-up GitHub issue to track the service-wide cleanup (replacing "subject", m.Msg.Subject with request_id + safe structured fields across all nats* handlers in room-service/handler.go)? Happy to do that now if it helps.


✏️ Learnings added
Learnt from: general-lex
Repo: hmchangw/chat PR: 247
File: room-service/handler.go:1480-1490
Timestamp: 2026-06-01T08:44:38.014Z
Learning: In hmchangw/chat, the established logging pattern across all `nats*` handler wrappers in `room-service/handler.go` is to include `"subject", m.Msg.Subject` in `slog.Error` calls (e.g., lines 119, 1399, 1484). Although this logs the raw NATS subject (which encodes account/room identifiers), it is the intentional, uniform pattern throughout the file. Do NOT flag individual handlers for this — any PII/log-shape fix must be done as a service-wide sweep across all handlers in a dedicated PR to preserve log consistency for operators.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Joey0538
Repo: hmchangw/chat PR: 59
File: pkg/roomkeysender/roomkeysender.go:28-30
Timestamp: 2026-04-08T08:25:32.108Z
Learning: In this repository’s Go code, follow the CLAUDE.md rule: don’t add defensive error handling, fallbacks, or nil/validation guards for conditions that are guaranteed not to occur. For internal functions/methods (e.g., when all callers are known to pass non-nil pointer parameters, such as pkg/roomkeysender/roomkeysender.go’s Send), the absence of nil-pointer guards is acceptable—do not flag it as a review issue. Reserve pointer validation and other safety checks for system boundaries (e.g., external inputs, API/HTTP requests, deserialization, or other untrusted entry points).

Learnt from: Joey0538
Repo: hmchangw/chat PR: 59
File: pkg/model/model_test.go:306-317
Timestamp: 2026-04-08T08:25:48.132Z
Learning: In Go, `time.Time` is `comparable` and therefore may be used as a type argument to generic constraints requiring `comparable` (e.g., `func roundTrip[T comparable](...)`). This is because `time.Time`’s underlying fields are all comparable (`uint64`, `int64`, and a pointer `*Location`, where pointers are comparable by address). As a result, any struct that contains or embeds a `time.Time` field (e.g., `model.Message`, or structs embedding it like `model.NotificationEvent`) should also satisfy `comparable` and should not be flagged as a compilation-error when used with `T comparable`.

Learnt from: general-lex
Repo: hmchangw/chat PR: 141
File: message-worker/main.go:95-99
Timestamp: 2026-05-04T03:30:38.678Z
Learning: In hmchangw/chat, `pkg/natsutil/connect.go`’s `natsutil.Connect` returns an OpenTelemetry-wrapped NATS connection (`*otelnats.Conn` from `github.com/Marz32onE/instrumentation-go/otel-nats/otelnats`), not a plain `*nats.Conn`. Because `otelnats.Conn.Publish` is defined as `Publish(ctx context.Context, subj string, data []byte) error` (it uses `ctx` to propagate traces via NATS headers), code review should NOT flag `nc.Publish(ctx, subj, data)` as an incorrect method call shape. This `Publish(ctx, ...)` pattern is the standard/correct usage across the codebase (e.g., message-worker, broadcast-worker, notification-worker, history-service).

Learnt from: general-lex
Repo: hmchangw/chat PR: 205
File: history-service/internal/service/pin.go:66-83
Timestamp: 2026-05-21T05:30:30.938Z
Learning: When reviewing cap enforcement in hmchangw/chat within history-service and room-service (e.g., functions like enforcePinLimit in history-service/internal/service/pin.go using MAX_* limits), the code may intentionally use a non-atomic read-then-write “soft cap” (allowing a small TOCTOU overage). Do not flag it as a TOCTOU/race-condition bug if the cap is explicitly designed to be operationally bounded rather than a hard invariant—i.e., it self-corrects because subsequent requests observe the inflated count and are rejected, and the codebase has not chosen atomic enforcement (e.g., LWT/counter table or findAndModify) as a requirement. Only flag as a TOCTOU issue when the surrounding codebase/design explicitly adopts atomic cap enforcement for these limits.

Comment thread room-service/handler.go
Comment on lines +1506 to +1559
sub, err := h.store.ToggleSubscriptionFavorite(ctx, roomID, account)
if err != nil {
if errors.Is(err, model.ErrSubscriptionNotFound) {
return nil, errNotRoomMember
}
return nil, fmt.Errorf("toggle subscription favorite: %w", err)
}

now := time.Now().UTC()

subEvt := model.SubscriptionUpdateEvent{
UserID: sub.User.ID,
Subscription: *sub,
Action: "favorite_toggled",
Timestamp: now.UnixMilli(),
}
subEvtData, err := json.Marshal(subEvt)
if err != nil {
return nil, fmt.Errorf("marshal subscription update event: %w", err)
}
if err := h.publishCore(ctx, subject.SubscriptionUpdate(account), subEvtData); err != nil {
slog.Error("subscription update publish failed", "error", err, "account", account)
// Non-fatal — the DB write is the source of truth; clients will reconcile on next refetch.
}

userSiteID, err := h.store.GetUserSiteID(ctx, account)
if err != nil {
return nil, fmt.Errorf("get user siteId: %w", err)
}
if userSiteID != "" && userSiteID != h.siteID {
payload := model.SubscriptionFavoriteToggledEvent{
Account: account,
RoomID: roomID,
Favorite: sub.Favorite,
Timestamp: now.UnixMilli(),
}
payloadData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal favorite-toggled payload: %w", err)
}
outbox := model.OutboxEvent{
Type: model.OutboxSubscriptionFavoriteToggled,
SiteID: h.siteID,
DestSiteID: userSiteID,
Payload: payloadData,
Timestamp: now.UnixMilli(),
}
outboxData, err := json.Marshal(outbox)
if err != nil {
return nil, fmt.Errorf("marshal outbox event: %w", err)
}
if err := h.publishToStream(ctx, subject.Outbox(h.siteID, userSiteID, model.OutboxSubscriptionFavoriteToggled), outboxData); err != nil {
return nil, fmt.Errorf("publish favorite-toggled outbox: %w", err)
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jun 1, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't return a retriable error after the toggle has already committed.

ToggleSubscriptionFavorite flips the bit before the outbox publish happens. If GetUserSiteID or publishToStream fails, the client gets an error even though the mutation already succeeded; a retry will flip favorite again and can leave the local state and federation stream out of sync. This needs an acknowledged-after-commit design, or a durable outbox/retry mechanism around the post-write fanout.

🤖 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 `@room-service/handler.go` around lines 1506 - 1559, The toggle currently
commits via h.store.ToggleSubscriptionFavorite then performs federation fanout
(GetUserSiteID, publishToStream) and returns an error if fanout fails, which can
re-trigger the toggle on retry; change to an acknowledged-after-commit pattern:
after ToggleSubscriptionFavorite succeeds, ensure outbox events are durably
recorded (e.g., insert an OutboxEvent row via a new store method like
CreateOutboxEvent or use an existing outbox writer) before attempting
publishToStream, and never return a retriable error to the client if outbox
publish fails; instead log the publish error and rely on a background worker to
read pending OutboxEvent entries and call publishToStream (or retry publish)
until success. Reference ToggleSubscriptionFavorite, GetUserSiteID,
publishToStream, model.OutboxEvent and the favorite-toggled outbox publish paths
when implementing durable outbox insertion and background retry.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verified — this trade-off is uniform across every cross-site publish in the system, not specific to favorite-toggle. mute.toggle, message.read, member.role.update, member.remove, add.member, and every other handler that writes to local Mongo and then publishes to OUTBOX use the same publish-then-error sequence.

A real transactional outbox is the right architectural answer eventually — Mongo outbox collection, new store method, idempotency keys, background drainer, dead-letter handling, and matching changes at every existing cross-site publish site. That's a multi-PR service-wide initiative, not a one-handler retrofit. Fixing only favorite.toggle would give the same Subscription document two different retry contracts depending on which boolean was mutated (favorite idempotent-on-retry, mute not), which is worse client-facing UX than the current uniform "client debounces" rule.

Leaving as-is to preserve sibling parity; tracking the durable-outbox initiative as a separate design discussion.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@general-lex, understood — the reasoning is sound. Applying a transactional-outbox retrofit to a single handler while all sibling handlers remain on the same "client debounces" contract would produce an inconsistent retry surface on the same Subscription document. Leaving this as-is for parity makes sense.

Would you like me to open a GitHub issue to track the service-wide durable-outbox initiative (Mongo outbox collection, background drainer, idempotency keys, dead-letter handling) so it doesn't get lost?


✏️ Learnings added
Learnt from: general-lex
Repo: hmchangw/chat PR: 247
File: room-service/handler.go:1506-1559
Timestamp: 2026-06-01T09:01:36.432Z
Learning: In hmchangw/chat, the pattern of committing a local Mongo write then publishing a cross-site OUTBOX event and returning an error to the client if the publish fails is uniform across ALL cross-site handlers (mute.toggle, message.read, member.role.update, member.remove, add.member, favorite.toggle, thread.read, etc.). This is an intentional architectural trade-off: the DB write is the source of truth and clients are expected to debounce retries. A proper durable-outbox solution (Mongo outbox collection, new store method, idempotency keys, background drainer, dead-letter handling) is tracked as a separate multi-PR service-wide initiative. Do NOT flag individual handlers for this pattern as it is consistent by design.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Joey0538
Repo: hmchangw/chat PR: 59
File: pkg/roomkeysender/roomkeysender.go:28-30
Timestamp: 2026-04-08T08:25:32.108Z
Learning: In this repository’s Go code, follow the CLAUDE.md rule: don’t add defensive error handling, fallbacks, or nil/validation guards for conditions that are guaranteed not to occur. For internal functions/methods (e.g., when all callers are known to pass non-nil pointer parameters, such as pkg/roomkeysender/roomkeysender.go’s Send), the absence of nil-pointer guards is acceptable—do not flag it as a review issue. Reserve pointer validation and other safety checks for system boundaries (e.g., external inputs, API/HTTP requests, deserialization, or other untrusted entry points).

Learnt from: Joey0538
Repo: hmchangw/chat PR: 59
File: pkg/model/model_test.go:306-317
Timestamp: 2026-04-08T08:25:48.132Z
Learning: In Go, `time.Time` is `comparable` and therefore may be used as a type argument to generic constraints requiring `comparable` (e.g., `func roundTrip[T comparable](...)`). This is because `time.Time`’s underlying fields are all comparable (`uint64`, `int64`, and a pointer `*Location`, where pointers are comparable by address). As a result, any struct that contains or embeds a `time.Time` field (e.g., `model.Message`, or structs embedding it like `model.NotificationEvent`) should also satisfy `comparable` and should not be flagged as a compilation-error when used with `T comparable`.

Learnt from: general-lex
Repo: hmchangw/chat PR: 141
File: message-worker/main.go:95-99
Timestamp: 2026-05-04T03:30:38.678Z
Learning: In hmchangw/chat, `pkg/natsutil/connect.go`’s `natsutil.Connect` returns an OpenTelemetry-wrapped NATS connection (`*otelnats.Conn` from `github.com/Marz32onE/instrumentation-go/otel-nats/otelnats`), not a plain `*nats.Conn`. Because `otelnats.Conn.Publish` is defined as `Publish(ctx context.Context, subj string, data []byte) error` (it uses `ctx` to propagate traces via NATS headers), code review should NOT flag `nc.Publish(ctx, subj, data)` as an incorrect method call shape. This `Publish(ctx, ...)` pattern is the standard/correct usage across the codebase (e.g., message-worker, broadcast-worker, notification-worker, history-service).

Learnt from: general-lex
Repo: hmchangw/chat PR: 205
File: history-service/internal/service/pin.go:66-83
Timestamp: 2026-05-21T05:30:30.938Z
Learning: When reviewing cap enforcement in hmchangw/chat within history-service and room-service (e.g., functions like enforcePinLimit in history-service/internal/service/pin.go using MAX_* limits), the code may intentionally use a non-atomic read-then-write “soft cap” (allowing a small TOCTOU overage). Do not flag it as a TOCTOU/race-condition bug if the cap is explicitly designed to be operationally bounded rather than a hard invariant—i.e., it self-corrects because subsequent requests observe the inflated count and are rejected, and the codebase has not chosen atomic enforcement (e.g., LWT/counter table or findAndModify) as a requirement. Only flag as a TOCTOU issue when the surrounding codebase/design explicitly adopts atomic cap enforcement for these limits.

@mliu33 mliu33 requested a review from vjauhari-work June 2, 2026 02:19
Mirrors the mute-toggle architecture for the new Favorite flag — a
per-user, room-scoped boolean rendered in the sidebar. room-service
flips Subscription.favorite atomically via a FindOneAndUpdate
aggregation pipeline (treats missing field as false), fans out a
subscription.update event with action="favorite_toggled" to the
requester's other sessions, and when the requester's home site differs
from the room's site, publishes a subscription_favorite_toggled
OutboxEvent that inbox-worker mirrors onto the home-site copy.

Backend treats favorite as a pure render hint — notification-worker
and broadcast-worker do not consult it.

https://claude.ai/code/session_01CNQTuBnbtUqQcoNKcF3i8f
@general-lex general-lex force-pushed the claude/focused-hypatia-gijQ6 branch from 0e6efba to 9c54abf Compare June 3, 2026 06:16
Comment thread pkg/model/subscription.go
Alert bool `json:"alert" bson:"alert"`
Muted bool `json:"muted" bson:"muted"`
Favorite bool `json:"favorite,omitempty" bson:"favorite,omitempty"`
Favorite bool `json:"favorite" bson:"favorite"`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need to remove omitempty flag here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

it is to match the convention of every other boolean state flag on Subscription (Alert, Muted, HasMention — all serialize-when-false), and I have published docs/client-api.md contract already like others for this field, is always present

Comment thread room-service/handler.go
if err != nil {
return nil, fmt.Errorf("marshal outbox event: %w", err)
}
if err := h.publishToStream(ctx, subject.Outbox(h.siteID, userSiteID, model.OutboxSubscriptionFavoriteToggled), outboxData); err != nil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There will be no more outbox and we are going to have one "INBOX_{siteID}" per site and publishing to it directly

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm aware there will be changes. But specifics are tentative. I think it's fine to keep things consistent in main, refactor can be done at once.

Copy link
Copy Markdown
Collaborator

@vjauhari-work vjauhari-work left a comment

Choose a reason for hiding this comment

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

Looks Great!

@general-lex general-lex merged commit 918a5f7 into main Jun 3, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants