@@ -198,6 +198,13 @@ type subscriberImpl[T any] struct {
198198
199199 mode ConcurrencyMode
200200 lockless bool
201+ // Per-subscription direct call helpers. When non-nil these are used in the
202+ // hot path to call the destination without additional interface dispatch
203+ // or context lookups. They are set once at subscription time by the
204+ // Observable (see observable.SubscribeWithContext).
205+ nextDirect func (context.Context , T )
206+ errorDirect func (context.Context , error )
207+ completeDirect func (context.Context )
201208}
202209
203210// Implements Observer.
@@ -213,7 +220,11 @@ func (s *subscriberImpl[T]) NextWithContext(ctx context.Context, v T) {
213220
214221 if s .lockless {
215222 if atomic .LoadInt32 (& s .status ) == 0 {
216- s .destination .NextWithContext (ctx , v )
223+ if s .nextDirect != nil {
224+ s .nextDirect (ctx , v )
225+ } else {
226+ s .destination .NextWithContext (ctx , v )
227+ }
217228 } else {
218229 OnDroppedNotification (ctx , NewNotificationNext (v ))
219230 }
@@ -231,7 +242,11 @@ func (s *subscriberImpl[T]) NextWithContext(ctx context.Context, v T) {
231242 }
232243
233244 if atomic .LoadInt32 (& s .status ) == 0 {
234- s .destination .NextWithContext (ctx , v )
245+ if s .nextDirect != nil {
246+ s .nextDirect (ctx , v )
247+ } else {
248+ s .destination .NextWithContext (ctx , v )
249+ }
235250 } else {
236251 OnDroppedNotification (ctx , NewNotificationNext (v ))
237252 }
@@ -249,7 +264,11 @@ func (s *subscriberImpl[T]) ErrorWithContext(ctx context.Context, err error) {
249264 if s .lockless {
250265 if atomic .CompareAndSwapInt32 (& s .status , 0 , 1 ) {
251266 if s .destination != nil {
252- s .destination .ErrorWithContext (ctx , err )
267+ if s .errorDirect != nil {
268+ s .errorDirect (ctx , err )
269+ } else {
270+ s .destination .ErrorWithContext (ctx , err )
271+ }
253272 }
254273 } else {
255274 OnDroppedNotification (ctx , NewNotificationError [T ](err ))
@@ -264,7 +283,11 @@ func (s *subscriberImpl[T]) ErrorWithContext(ctx context.Context, err error) {
264283
265284 if atomic .CompareAndSwapInt32 (& s .status , 0 , 1 ) {
266285 if s .destination != nil {
267- s .destination .ErrorWithContext (ctx , err )
286+ if s .errorDirect != nil {
287+ s .errorDirect (ctx , err )
288+ } else {
289+ s .destination .ErrorWithContext (ctx , err )
290+ }
268291 }
269292 } else {
270293 OnDroppedNotification (ctx , NewNotificationError [T ](err ))
@@ -285,7 +308,11 @@ func (s *subscriberImpl[T]) CompleteWithContext(ctx context.Context) {
285308 if s .lockless {
286309 if atomic .CompareAndSwapInt32 (& s .status , 0 , 2 ) {
287310 if s .destination != nil {
288- s .destination .CompleteWithContext (ctx )
311+ if s .completeDirect != nil {
312+ s .completeDirect (ctx )
313+ } else {
314+ s .destination .CompleteWithContext (ctx )
315+ }
289316 }
290317 } else {
291318 OnDroppedNotification (ctx , NewNotificationComplete [T ]())
@@ -300,7 +327,11 @@ func (s *subscriberImpl[T]) CompleteWithContext(ctx context.Context) {
300327
301328 if atomic .CompareAndSwapInt32 (& s .status , 0 , 2 ) {
302329 if s .destination != nil {
303- s .destination .CompleteWithContext (ctx )
330+ if s .completeDirect != nil {
331+ s .completeDirect (ctx )
332+ } else {
333+ s .destination .CompleteWithContext (ctx )
334+ }
304335 }
305336 } else {
306337 OnDroppedNotification (ctx , NewNotificationComplete [T ]())
@@ -337,3 +368,21 @@ func (s *subscriberImpl[T]) unsubscribe() {
337368 // s.Subscription.Unsubscribe() is protected against concurrent calls.
338369 s .Subscription .Unsubscribe ()
339370}
371+
372+ // setDirectors configures per-subscription direct call helpers based on the
373+ // concrete destination type and the precomputed capture flag. This avoids
374+ // per-notification context lookups and type assertions on the hot path.
375+ func (s * subscriberImpl [T ]) setDirectors (destination Observer [T ], capture bool ) {
376+ // Default to interface-based calls.
377+ s .nextDirect = func (ctx context.Context , v T ) { destination .NextWithContext (ctx , v ) }
378+ s .errorDirect = func (ctx context.Context , err error ) { destination .ErrorWithContext (ctx , err ) }
379+ s .completeDirect = func (ctx context.Context ) { destination .CompleteWithContext (ctx ) }
380+
381+ // If destination is an *observerImpl[T], we can call internal helpers that
382+ // accept a precomputed capture flag and therefore avoid context lookups.
383+ if oi , ok := destination .(* observerImpl [T ]); ok {
384+ s .nextDirect = func (ctx context.Context , v T ) { oi .tryNextWithCapture (ctx , v , capture ) }
385+ s .errorDirect = func (ctx context.Context , err error ) { oi .tryErrorWithCapture (ctx , err , capture ) }
386+ s .completeDirect = func (ctx context.Context ) { oi .tryCompleteWithCapture (ctx , capture ) }
387+ }
388+ }
0 commit comments