Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion social/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type Client struct {

subscriptionMu sync.RWMutex
subscription *rta.Subscription
subscriptionHandlers []SubscriptionHandler
subscriptionHandlers []*handlerRegistration
}

// Close closes the Client with a context of 15 seconds timeout.
Expand Down
49 changes: 34 additions & 15 deletions social/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,52 @@ import (
"github.com/df-mc/go-xsapi/v2/rta"
)

// Subscribe subscribes to RTA (Real-Time Activity) services to receive
// notifications for changes in the caller's friend list.
//
// The provided [SubscriptionHandler] is used to dispatch events delivered
// over the RTA subscription, such as when a user adds or removes the caller.
// handlerRegistration gives each Subscribe call its own identity without
// comparing handler values. Entries stay immutable while callbacks use them.
type handlerRegistration struct {
SubscriptionHandler
}

// Subscribe registers h for real-time changes to the caller's friend list and
// returns a function that removes this registration. Registrations share one
// RTA subscription, which is released when the last registration is removed.
// Registering the same handler twice creates two independent registrations.
//
// The RTA subscription is created on the first call and cached internally
// to avoid exceeding RTA's maximum subscription limit. Subsequent calls
// reuse the existing subscription and append h to the list of active handlers.
// The cleanup function is safe to call repeatedly or concurrently. It removes
// the local registration even if RTA teardown fails; calling it again retries
// teardown. Callbacks already queued may still run after cleanup returns.
// [Client.CloseContext] removes all registrations at once.
//
// Subscribe returns an error if h is nil.
func (c *Client) Subscribe(ctx context.Context, h SubscriptionHandler) (err error) {
// If h is nil or subscribing fails, Subscribe returns a nil cleanup function
// and an error.
func (c *Client) Subscribe(ctx context.Context, h SubscriptionHandler) (func(context.Context) error, error) {
if h == nil {
return errors.New("xsapi/social: cannot subscribe with a nil SubscriptionHandler")
return nil, errors.New("xsapi/social: cannot subscribe with a nil SubscriptionHandler")
}

c.subscriptionMu.Lock()
defer c.subscriptionMu.Unlock()

if !c.subscription.Active() {
if err := c.rta.Subscribe(ctx, c.subscription); err != nil {
return err
return nil, err
}
}

c.subscriptionHandlers = append(c.subscriptionHandlers, h)
return nil
registration := &handlerRegistration{h}
c.subscriptionHandlers = append(c.subscriptionHandlers, registration)
return func(ctx context.Context) error {
c.subscriptionMu.Lock()
defer c.subscriptionMu.Unlock()

if i := slices.Index(c.subscriptionHandlers, registration); i >= 0 {
c.subscriptionHandlers = slices.Delete(c.subscriptionHandlers, i, i+1)
}
if len(c.subscriptionHandlers) > 0 || !c.subscription.Active() {
return nil
}
return c.rta.Unsubscribe(ctx, c.subscription)
}, nil
}

// subscriptionHandler is an internal implementation of [rta.SubscriptionHandler]
Expand Down Expand Up @@ -119,7 +138,7 @@ func (h *subscriptionHandler) HandleError(err error) {
}
}

func (h *subscriptionHandler) handlers() []SubscriptionHandler {
func (h *subscriptionHandler) handlers() []*handlerRegistration {
h.subscriptionMu.RLock()
defer h.subscriptionMu.RUnlock()
return slices.Clone(h.subscriptionHandlers)
Expand Down
24 changes: 19 additions & 5 deletions social/subscription_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import (
func TestSubscribeWithoutRTAFails(t *testing.T) {
client := New(http.DefaultClient, nil, xsts.UserInfo{XUID: "1"}, nil)

err := client.Subscribe(context.Background(), NopSubscriptionHandler{})
if !errors.Is(err, rta.ErrUnavailable) {
cleanup, err := client.Subscribe(context.Background(), NopSubscriptionHandler{})
if cleanup != nil || !errors.Is(err, rta.ErrUnavailable) {
t.Fatalf("Subscribe error = %v, want %v", err, rta.ErrUnavailable)
}
}
Expand All @@ -31,7 +31,7 @@ func TestSubscriptionHandlerAllowsNonComparableHandlers(t *testing.T) {
data: []string{"non-comparable"},
}
c := &Client{
subscriptionHandlers: []SubscriptionHandler{handler},
subscriptionHandlers: []*handlerRegistration{{handler}},
}
h := &subscriptionHandler{
Client: c,
Expand All @@ -57,7 +57,7 @@ func TestSubscriptionHandlerIgnoresUserUnsubscribe(t *testing.T) {
data: []string{"non-comparable"},
}
c := &Client{
subscriptionHandlers: []SubscriptionHandler{handler},
subscriptionHandlers: []*handlerRegistration{{handler}},
}
h := &subscriptionHandler{
Client: c,
Expand All @@ -80,7 +80,7 @@ func TestSubscriptionHandlerNotifiesSubscriptionLost(t *testing.T) {
data: []string{"non-comparable"},
}
c := &Client{
subscriptionHandlers: []SubscriptionHandler{handler},
subscriptionHandlers: []*handlerRegistration{{handler}},
}
h := &subscriptionHandler{
Client: c,
Expand Down Expand Up @@ -113,3 +113,17 @@ func (h nonComparableSocialHandler) HandleIncomingFriendRequestCountChange(int)
func (h nonComparableSocialHandler) HandleSubscriptionLost() {
h.calls <- "lost"
}

// interfaceSocialHandler carries arbitrary data without implementing comparisons.
type interfaceSocialHandler struct {
NopSubscriptionHandler
data any
}

func TestSubscribeRejectsNilHandler(t *testing.T) {
c := New(http.DefaultClient, nil, xsts.UserInfo{XUID: "1"}, nil)
cleanup, err := c.Subscribe(t.Context(), nil)
if err == nil || cleanup != nil {
t.Fatalf("Subscribe(nil) returned cleanup=%v, error=%v", cleanup != nil, err)
}
}
Loading