From 42d630c612fe9f55972b89803a2532d43d0397bb Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Fri, 10 Jul 2026 10:26:23 -0300 Subject: [PATCH 1/2] feat: add stream close reason to social service v2 subscription updates Adds a SubscriptionStreamClosed message (reason enum + optional detail) and an optional stream_closed field to all six streamed update messages, so the server can send a final message informing the client why a subscription stream is being closed. When stream_closed is present, the message carries no update data and the stream ends right after it. Wire- and JSON-compatible: new fields only (verified with buf breaking). --- .../social_service/v2/social_service_v2.proto | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/proto/decentraland/social_service/v2/social_service_v2.proto b/proto/decentraland/social_service/v2/social_service_v2.proto index 5cddad29..7425d1fb 100644 --- a/proto/decentraland/social_service/v2/social_service_v2.proto +++ b/proto/decentraland/social_service/v2/social_service_v2.proto @@ -112,6 +112,34 @@ message UpsertFriendshipResponse { } } +// Subscription stream lifecycle + +// Reason codes for a server-initiated closure of a subscription stream. +enum SubscriptionStreamClosedReason { + // The server did not specify a finer-grained reason. + STREAM_CLOSED_UNKNOWN = 0; + // The server is shutting down or draining its connections. + STREAM_CLOSED_SERVER_SHUTTING_DOWN = 1; + // The connection already has an active subscription for this stream. + STREAM_CLOSED_DUPLICATE_SUBSCRIPTION = 2; + // The connection is no longer authorized to keep the stream open. + STREAM_CLOSED_UNAUTHORIZED = 3; + // The server cleaned up the subscription after detecting its state was stale. + STREAM_CLOSED_STALE_SUBSCRIPTION = 4; + // An unrecoverable server-side error ended the stream. + STREAM_CLOSED_INTERNAL_ERROR = 5; +} + +// Sent by the server as the FINAL message of a subscription stream to inform the +// client why the stream is being closed. When the `stream_closed` field is present +// on a streamed update, the message carries no update data and the stream ends +// right after it. +message SubscriptionStreamClosed { + SubscriptionStreamClosedReason reason = 1; + // Optional human-readable detail, meant for logging/debugging (not for UI). + optional string message = 2; +} + message FriendshipUpdate { message RequestResponse { FriendProfile friend = 1; @@ -133,11 +161,19 @@ message FriendshipUpdate { CancelResponse cancel = 5; BlockResponse block = 6; } + + // Present only on the final message of the stream; explains why the server closed it. + // When set, the `update` oneof is unset. + optional SubscriptionStreamClosed stream_closed = 7; } message FriendConnectivityUpdate { FriendProfile friend = 1; ConnectivityStatus status = 2; + + // Present only on the final message of the stream; explains why the server closed it. + // When set, all other fields are unset. + optional SubscriptionStreamClosed stream_closed = 3; } message GetFriendshipStatusPayload { @@ -288,12 +324,20 @@ message GetBlockingStatusResponse { message BlockUpdate { string address = 1; bool is_blocked = 2; + + // Present only on the final message of the stream; explains why the server closed it. + // When set, all other fields are unset. + optional SubscriptionStreamClosed stream_closed = 3; } message CommunityMemberConnectivityUpdate { string community_id = 1; User member = 2; ConnectivityStatus status = 3; + + // Present only on the final message of the stream; explains why the server closed it. + // When set, all other fields are unset. + optional SubscriptionStreamClosed stream_closed = 4; } // Private voice chats @@ -378,6 +422,10 @@ message PrivateVoiceChatUpdate { optional User caller = 3; optional User callee = 4; optional PrivateVoiceChatCredentials credentials = 5; + + // Present only on the final message of the stream; explains why the server closed it. + // When set, all other fields are unset. + optional SubscriptionStreamClosed stream_closed = 6; } // Ending a private voice chat @@ -618,6 +666,10 @@ message CommunityVoiceChatUpdate { string community_name = 7; // Name of the community optional string community_image = 8; // Image/picture of the community repeated string worlds = 9; // World names associated with the community (world: true) + + // Present only on the final message of the stream; explains why the server closed it. + // When set, all other fields are unset. + optional SubscriptionStreamClosed stream_closed = 10; } service SocialService { From c419dba0182e72ecc1473f4520e0afa9eb3874c4 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Fri, 10 Jul 2026 12:24:52 -0300 Subject: [PATCH 2/2] refactor: keep only the deliverable subscription stream close reason A close notice reaches the client as the final stream message, which requires the connection to still be alive. That only holds for the duplicate-subscription rejection; shutdown and stale-cleanup closes happen after the socket is already gone, so those reasons could never be delivered. Trim the enum to UNKNOWN and DUPLICATE_SUBSCRIPTION. --- .../social_service/v2/social_service_v2.proto | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/proto/decentraland/social_service/v2/social_service_v2.proto b/proto/decentraland/social_service/v2/social_service_v2.proto index 7425d1fb..ee8dbba0 100644 --- a/proto/decentraland/social_service/v2/social_service_v2.proto +++ b/proto/decentraland/social_service/v2/social_service_v2.proto @@ -114,20 +114,16 @@ message UpsertFriendshipResponse { // Subscription stream lifecycle -// Reason codes for a server-initiated closure of a subscription stream. +// Reason codes for a server-initiated closure of a subscription stream. Only reasons that +// can actually be delivered are listed: a close notice reaches the client as the final +// stream message, which requires the connection to still be alive. That rules out +// shutdown/stale-cleanup closes, where the socket is already gone before the server tears +// the subscription down. enum SubscriptionStreamClosedReason { // The server did not specify a finer-grained reason. STREAM_CLOSED_UNKNOWN = 0; - // The server is shutting down or draining its connections. - STREAM_CLOSED_SERVER_SHUTTING_DOWN = 1; // The connection already has an active subscription for this stream. - STREAM_CLOSED_DUPLICATE_SUBSCRIPTION = 2; - // The connection is no longer authorized to keep the stream open. - STREAM_CLOSED_UNAUTHORIZED = 3; - // The server cleaned up the subscription after detecting its state was stale. - STREAM_CLOSED_STALE_SUBSCRIPTION = 4; - // An unrecoverable server-side error ended the stream. - STREAM_CLOSED_INTERNAL_ERROR = 5; + STREAM_CLOSED_DUPLICATE_SUBSCRIPTION = 1; } // Sent by the server as the FINAL message of a subscription stream to inform the