@@ -169,7 +169,7 @@ def initialize(
169169 @allowed_origins = Array ( allowed_origins ) . map ( &:downcase ) . freeze
170170 @pending_responses = { }
171171
172- # Maps a `subscriptions/listen` request id to `{ stream: stream_object, filter: honored_subscription_filter }` (SEP-2575).
172+ # Maps a `subscriptions/listen` request id to `{ stream: stream_object, filter: honored_subscription_filter, active: boolean }` (SEP-2575).
173173 # In-process only; a multi-worker deployment needs an external event bus to fan notifications out across processes,
174174 # which is a follow-up.
175175 @listen_subscriptions = { }
@@ -922,6 +922,12 @@ def first
922922
923923 # The body registers the stream and returns, leaving the response open like
924924 # the legacy GET stream (`create_sse_body`).
925+ #
926+ # Registration and activation are split on purpose: the entry is inserted inactive
927+ # (reserving the id and the cap slot atomically), the acknowledgement is written outside the lock,
928+ # and only then does the entry become eligible for delivery. A concurrent notification between
929+ # the insert and the acknowledgement write skips the inactive entry,
930+ # enforcing the SEP-2575 rule that no notification precedes the acknowledgement.
925931 def listen_sse_body ( request_id , honored )
926932 ListenStreamBody . new do |stream |
927933 rejected = false
@@ -930,7 +936,7 @@ def listen_sse_body(request_id, honored)
930936 ( @max_listen_subscriptions && @listen_subscriptions . size >= @max_listen_subscriptions )
931937 rejected = true
932938 else
933- @listen_subscriptions [ request_id ] = { stream : stream , filter : honored }
939+ @listen_subscriptions [ request_id ] = { stream : stream , filter : honored , active : false }
934940 end
935941 end
936942
@@ -948,6 +954,7 @@ def listen_sse_body(request_id, honored)
948954
949955 begin
950956 send_to_stream ( stream , acknowledgement )
957+ activate_listen_subscription ( request_id )
951958 start_listen_keepalive_thread ( request_id )
952959 rescue *STREAM_WRITE_ERRORS
953960 remove_listen_subscription ( request_id )
@@ -957,6 +964,15 @@ def listen_sse_body(request_id, honored)
957964 end
958965 end
959966
967+ # Marks a listen subscription eligible for delivery once its acknowledgement write has completed.
968+ # The entry may already be gone when the transport closed concurrently.
969+ def activate_listen_subscription ( request_id )
970+ @mutex . synchronize do
971+ subscription = @listen_subscriptions [ request_id ]
972+ subscription [ :active ] = true if subscription
973+ end
974+ end
975+
960976 # Periodically writes an SSE keepalive comment frame to a listen stream so a silently dropped
961977 # connection is detected and its slot freed, rather than held until the next fan-out write.
962978 # Mirrors the legacy GET stream's `start_keepalive_thread`; a comment frame (not a data frame)
@@ -1038,6 +1054,10 @@ def deliver_to_listen_subscriptions(method, params)
10381054 # a slow or stalled subscriber must not block the transport, matching the legacy delivery paths.
10391055 matched = @mutex . synchronize do
10401056 @listen_subscriptions . filter_map do |request_id , subscription |
1057+ # An inactive entry has not finished writing its acknowledgement yet;
1058+ # delivering to it would put a notification ahead of the acknowledgement.
1059+ next unless subscription [ :active ]
1060+
10411061 hit = if field
10421062 subscription [ :filter ] [ field ]
10431063 else
0 commit comments