com.wallstop-studios.dxmessaging 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +286 -0
  2. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +3 -3
  3. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +3 -3
  4. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +3 -3
  5. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +3 -3
  6. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +3 -3
  7. package/Editor/DxMessagingMenu.cs.meta +3 -3
  8. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +3 -3
  9. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +2 -3
  10. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +3 -3
  11. package/Editor/SetupCscRsp.cs +232 -163
  12. package/README.md +1 -1
  13. package/Runtime/Analyzers.meta +8 -0
  14. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +3 -3
  15. package/Runtime/Core/DxMessagingStaticState.cs.meta +3 -3
  16. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +3 -3
  17. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +3 -3
  18. package/Runtime/Core/IMessage.cs.meta +3 -3
  19. package/Runtime/Core/InstanceId.cs.meta +3 -3
  20. package/Runtime/Core/Internal/TypedSlots.cs +42 -0
  21. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +3 -3
  22. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +3 -3
  23. package/Runtime/Core/MessageBus/IMessageBus.cs +63 -28
  24. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +3 -3
  25. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +3 -3
  26. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +3 -3
  27. package/Runtime/Core/MessageBus/MessageBus.cs +548 -474
  28. package/Runtime/Core/MessageBus/MessageBus.cs.meta +3 -3
  29. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +3 -3
  30. package/Runtime/Core/MessageBus/MessageBusRegistration.cs +207 -0
  31. package/Runtime/Core/MessageBus/MessageBusRegistration.cs.meta +11 -0
  32. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +3 -3
  33. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +3 -3
  34. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +3 -3
  35. package/Runtime/Core/MessageHandler.cs +1210 -394
  36. package/Runtime/Core/MessageHandler.cs.meta +3 -3
  37. package/Runtime/Core/MessageRegistrationHandle.cs.meta +3 -3
  38. package/Runtime/Core/MessageRegistrationToken.cs +1296 -819
  39. package/Runtime/Core/MessageRegistrationToken.cs.meta +3 -3
  40. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +3 -3
  41. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +3 -3
  42. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +3 -3
  43. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +3 -3
  44. package/Runtime/Core/MessagingDebug.cs.meta +3 -3
  45. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +3 -3
  46. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +3 -3
  47. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +3 -3
  48. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +3 -3
  49. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +3 -3
  50. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +3 -3
  51. package/Runtime/Unity/MessageAwareComponent.cs.meta +3 -3
  52. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +3 -3
  53. package/Runtime/Unity/MessagingComponent.cs.meta +3 -3
  54. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +3 -3
  55. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +3 -3
  56. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +3 -3
  57. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +3 -3
  58. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +3 -3
  59. package/Samples~/Mini Combat/Boot.cs.meta +3 -3
  60. package/Samples~/Mini Combat/Enemy.cs.meta +3 -3
  61. package/Samples~/Mini Combat/Messages.cs.meta +3 -3
  62. package/Samples~/Mini Combat/Player.cs.meta +3 -3
  63. package/Samples~/Mini Combat/UIOverlay.cs.meta +3 -3
  64. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +3 -3
  65. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +3 -3
  66. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +3 -3
  67. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +3 -3
  68. package/SourceGenerators/Directory.Build.props +5 -5
  69. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +3 -3
  70. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +12 -11
  71. package/package.json +6 -3
  72. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  73. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +0 -0
  74. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
  75. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll.meta +0 -0
  76. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll +0 -0
  77. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll.meta +0 -0
  78. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll +0 -0
  79. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll.meta +0 -0
  80. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  81. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +0 -0
  82. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  83. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +0 -0
  84. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  85. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +0 -0
@@ -2562,7 +2562,10 @@ namespace DxMessaging.Core.MessageBus
2562
2562
  }
2563
2563
 
2564
2564
  /// <inheritdoc />
2565
- public Action RegisterUntargeted<T>(MessageHandler messageHandler, int priority = 0)
2565
+ public MessageBusRegistration RegisterUntargeted<T>(
2566
+ MessageHandler messageHandler,
2567
+ int priority = 0
2568
+ )
2566
2569
  where T : IUntargetedMessage
2567
2570
  {
2568
2571
  EnsureAotUntargetedBridge<T>();
@@ -2575,7 +2578,7 @@ namespace DxMessaging.Core.MessageBus
2575
2578
  }
2576
2579
 
2577
2580
  /// <inheritdoc />
2578
- public Action RegisterTargeted<T>(
2581
+ public MessageBusRegistration RegisterTargeted<T>(
2579
2582
  InstanceId target,
2580
2583
  MessageHandler messageHandler,
2581
2584
  int priority = 0
@@ -2593,7 +2596,7 @@ namespace DxMessaging.Core.MessageBus
2593
2596
  }
2594
2597
 
2595
2598
  /// <inheritdoc />
2596
- public Action RegisterSourcedBroadcast<T>(
2599
+ public MessageBusRegistration RegisterSourcedBroadcast<T>(
2597
2600
  InstanceId source,
2598
2601
  MessageHandler messageHandler,
2599
2602
  int priority = 0
@@ -2611,7 +2614,7 @@ namespace DxMessaging.Core.MessageBus
2611
2614
  }
2612
2615
 
2613
2616
  /// <inheritdoc />
2614
- public Action RegisterSourcedBroadcastWithoutSource<T>(
2617
+ public MessageBusRegistration RegisterSourcedBroadcastWithoutSource<T>(
2615
2618
  MessageHandler messageHandler,
2616
2619
  int priority = 0
2617
2620
  )
@@ -2627,7 +2630,7 @@ namespace DxMessaging.Core.MessageBus
2627
2630
  }
2628
2631
 
2629
2632
  /// <inheritdoc />
2630
- public Action RegisterTargetedWithoutTargeting<T>(
2633
+ public MessageBusRegistration RegisterTargetedWithoutTargeting<T>(
2631
2634
  MessageHandler messageHandler,
2632
2635
  int priority = 0
2633
2636
  )
@@ -2643,7 +2646,7 @@ namespace DxMessaging.Core.MessageBus
2643
2646
  }
2644
2647
 
2645
2648
  /// <inheritdoc />
2646
- public Action RegisterGlobalAcceptAll(MessageHandler messageHandler)
2649
+ public MessageBusRegistration RegisterGlobalAcceptAll(MessageHandler messageHandler)
2647
2650
  {
2648
2651
  long touchTick = AdvanceTick();
2649
2652
  InvalidateDispatchPlans();
@@ -2689,88 +2692,107 @@ namespace DxMessaging.Core.MessageBus
2689
2692
 
2690
2693
  long capturedGeneration = _resetGeneration;
2691
2694
  long capturedSweepGeneration = _globalSlotSweepGeneration;
2692
- return () =>
2695
+ return new MessageBusRegistration(
2696
+ MessageBusRegistration.Kind.GlobalAcceptAll,
2697
+ RegistrationMethod.GlobalAcceptAll,
2698
+ 0,
2699
+ capturedGeneration,
2700
+ capturedSweepGeneration,
2701
+ touchTick,
2702
+ null,
2703
+ null,
2704
+ messageHandler,
2705
+ default
2706
+ );
2707
+ }
2708
+
2709
+ /// <summary>
2710
+ /// Re-expression of the former <see cref="RegisterGlobalAcceptAll"/> closure body: the
2711
+ /// global accept-all deregistration. Behaviour and both generation guards are unchanged.
2712
+ /// </summary>
2713
+ private void DeregisterGlobalAcceptAll(in MessageBusRegistration reg)
2714
+ {
2715
+ // Generation guard: see DeregisterScalarHandler. The global slot guards on BOTH the
2716
+ // reset generation and the global-slot sweep generation.
2717
+ if (
2718
+ reg.generation != _resetGeneration
2719
+ || reg.sweepGeneration != _globalSlotSweepGeneration
2720
+ )
2693
2721
  {
2694
- // Generation guard: see InternalRegisterUntargeted for the
2695
- // rationale. Skip silently when the closure outlived a Reset.
2696
- if (
2697
- capturedGeneration != _resetGeneration
2698
- || capturedSweepGeneration != _globalSlotSweepGeneration
2722
+ return;
2723
+ }
2724
+
2725
+ MessageHandler messageHandler = (MessageHandler)reg.payload;
2726
+ Type type = typeof(IMessage);
2727
+
2728
+ long deregisterTouchTick = AdvanceTick();
2729
+ InvalidateDispatchPlans();
2730
+ _globalSlots.version++;
2731
+ _log.Log(
2732
+ new MessagingRegistration(
2733
+ messageHandler.owner,
2734
+ type,
2735
+ RegistrationType.Deregister,
2736
+ RegistrationMethod.GlobalAcceptAll
2699
2737
  )
2738
+ );
2739
+ if (!_globalSlots.sharedHandlers.TryGetValue(messageHandler, out int count))
2740
+ {
2741
+ if (MessagingDebug.enabled)
2700
2742
  {
2701
- return;
2743
+ MessagingDebug.Log(
2744
+ LogLevel.Error,
2745
+ "Received over-deregistration of GlobalAcceptAll for MessageHandler {0}. Check to make sure you're not calling (de)registration multiple times.",
2746
+ messageHandler
2747
+ );
2702
2748
  }
2703
2749
 
2704
- long deregisterTouchTick = AdvanceTick();
2705
- InvalidateDispatchPlans();
2706
- _globalSlots.version++;
2707
- _log.Log(
2708
- new MessagingRegistration(
2709
- messageHandler.owner,
2710
- type,
2711
- RegistrationType.Deregister,
2712
- RegistrationMethod.GlobalAcceptAll
2713
- )
2714
- );
2715
- if (!_globalSlots.sharedHandlers.TryGetValue(messageHandler, out count))
2716
- {
2717
- if (MessagingDebug.enabled)
2718
- {
2719
- MessagingDebug.Log(
2720
- LogLevel.Error,
2721
- "Received over-deregistration of GlobalAcceptAll for MessageHandler {0}. Check to make sure you're not calling (de)registration multiple times.",
2722
- messageHandler
2723
- );
2724
- }
2725
-
2726
- return;
2727
- }
2750
+ return;
2751
+ }
2728
2752
 
2729
- _globalSlots.lastTouchTicks = deregisterTouchTick;
2730
- if (count <= 1)
2731
- {
2732
- _ = _globalSlots.sharedHandlers.Remove(messageHandler);
2733
- MarkDirtyHandler(messageHandler);
2734
- _globalSlotSweepCandidate = true;
2735
- // Final-removal of this handler from sharedHandlers is the
2736
- // 1 -> 0 transition that mirrors back into liveCount.
2737
- // Partial deregistration (count > 1) leaves liveCount
2738
- // alone -- the dictionary entry is still present.
2739
- _globalSlots.liveCount--;
2740
- }
2741
- else
2742
- {
2743
- _globalSlots.sharedHandlers[messageHandler] = count - 1;
2744
- }
2753
+ _globalSlots.lastTouchTicks = deregisterTouchTick;
2754
+ if (count <= 1)
2755
+ {
2756
+ _ = _globalSlots.sharedHandlers.Remove(messageHandler);
2757
+ MarkDirtyHandler(messageHandler);
2758
+ _globalSlotSweepCandidate = true;
2759
+ // Final-removal of this handler from sharedHandlers is the 1 -> 0 transition that
2760
+ // mirrors back into liveCount. Partial deregistration (count > 1) leaves liveCount
2761
+ // alone -- the dictionary entry is still present.
2762
+ _globalSlots.liveCount--;
2763
+ }
2764
+ else
2765
+ {
2766
+ _globalSlots.sharedHandlers[messageHandler] = count - 1;
2767
+ }
2745
2768
 
2746
- StageGlobalDispatchSnapshot<IUntargetedMessage>(
2747
- this,
2748
- _globalSlots,
2749
- DispatchKind.Untargeted
2750
- );
2751
- StageGlobalDispatchSnapshot<ITargetedMessage>(
2752
- this,
2753
- _globalSlots,
2754
- DispatchKind.Targeted
2755
- );
2756
- StageGlobalDispatchSnapshot<IBroadcastMessage>(
2757
- this,
2758
- _globalSlots,
2759
- DispatchKind.Broadcast
2760
- );
2761
- DebugAssertGlobalLiveCount();
2762
- };
2769
+ StageGlobalDispatchSnapshot<IUntargetedMessage>(
2770
+ this,
2771
+ _globalSlots,
2772
+ DispatchKind.Untargeted
2773
+ );
2774
+ StageGlobalDispatchSnapshot<ITargetedMessage>(
2775
+ this,
2776
+ _globalSlots,
2777
+ DispatchKind.Targeted
2778
+ );
2779
+ StageGlobalDispatchSnapshot<IBroadcastMessage>(
2780
+ this,
2781
+ _globalSlots,
2782
+ DispatchKind.Broadcast
2783
+ );
2784
+ DebugAssertGlobalLiveCount();
2763
2785
  }
2764
2786
 
2765
2787
  /// <inheritdoc />
2766
- public Action RegisterUntargetedInterceptor<T>(
2788
+ public MessageBusRegistration RegisterUntargetedInterceptor<T>(
2767
2789
  UntargetedInterceptor<T> interceptor,
2768
2790
  int priority = 0
2769
2791
  )
2770
2792
  where T : IUntargetedMessage
2771
2793
  {
2772
2794
  EnsureAotUntargetedBridge<T>();
2773
- _ = AdvanceTick();
2795
+ long touchTick = AdvanceTick();
2774
2796
  InvalidateDispatchPlans();
2775
2797
  InterceptorCache<object> prioritizedInterceptors =
2776
2798
  _untargetedInterceptsByType.GetOrAdd<T>();
@@ -2819,107 +2841,29 @@ namespace DxMessaging.Core.MessageBus
2819
2841
  );
2820
2842
 
2821
2843
  long capturedGeneration = _resetGeneration;
2822
- return () =>
2823
- {
2824
- // Generation guard: see InternalRegisterUntargeted for the
2825
- // rationale. Skip silently when the closure outlived a Reset.
2826
- if (capturedGeneration != _resetGeneration)
2827
- {
2828
- return;
2829
- }
2830
- if (
2831
- IsStaleInterceptorDeregisterAfterSweep<T>(
2832
- _untargetedInterceptsByType,
2833
- capturedInterceptors
2834
- )
2835
- )
2836
- {
2837
- return;
2838
- }
2839
-
2840
- _ = AdvanceTick();
2841
- InvalidateDispatchPlans();
2842
- prioritizedInterceptors.lastTouchTicks = _tickCounter;
2843
- MarkDirtyType<T>();
2844
- _log.Log(
2845
- new MessagingRegistration(
2846
- InstanceId.EmptyId,
2847
- type,
2848
- RegistrationType.Deregister,
2849
- RegistrationMethod.Interceptor
2850
- )
2851
- );
2852
- bool removed = false;
2853
- if (_uniqueInterceptorsAndPriorities.TryGetValue(interceptor, out priorityCount))
2854
- {
2855
- if (priorityCount.TryGetValue(priority, out count))
2856
- {
2857
- if (1 < count)
2858
- {
2859
- priorityCount[priority] = count - 1;
2860
- }
2861
- else
2862
- {
2863
- removed = true;
2864
- _ = priorityCount.Remove(priority);
2865
- }
2866
- }
2867
-
2868
- if (priorityCount.Count == 0)
2869
- {
2870
- _uniqueInterceptorsAndPriorities.Remove(interceptor);
2871
- }
2872
- }
2873
- else if (MessagingDebug.enabled)
2874
- {
2875
- MessagingDebug.Log(
2876
- LogLevel.Error,
2877
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
2878
- interceptor
2879
- );
2880
- }
2881
-
2882
- bool complete = false;
2883
- if (removed)
2884
- {
2885
- if (_untargetedInterceptsByType.TryGetValue<T>(out prioritizedInterceptors))
2886
- {
2887
- if (
2888
- prioritizedInterceptors.handlers.TryGetValue(
2889
- priority,
2890
- out List<object> interceptors
2891
- )
2892
- )
2893
- {
2894
- complete = interceptors.Remove(interceptor);
2895
- if (interceptors.Count == 0)
2896
- {
2897
- _ = prioritizedInterceptors.handlers.Remove(priority);
2898
- }
2899
- }
2900
- }
2901
-
2902
- if (!complete && MessagingDebug.enabled)
2903
- {
2904
- MessagingDebug.Log(
2905
- LogLevel.Error,
2906
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
2907
- interceptor
2908
- );
2909
- }
2910
- }
2911
- };
2844
+ return new MessageBusRegistration(
2845
+ MessageBusRegistration.Kind.UntargetedInterceptor,
2846
+ RegistrationMethod.Interceptor,
2847
+ priority,
2848
+ capturedGeneration,
2849
+ 0L,
2850
+ touchTick,
2851
+ capturedInterceptors,
2852
+ null,
2853
+ interceptor,
2854
+ default
2855
+ );
2912
2856
  }
2913
2857
 
2914
2858
  /// <inheritdoc />
2915
- public Action RegisterTargetedInterceptor<T>(
2859
+ public MessageBusRegistration RegisterTargetedInterceptor<T>(
2916
2860
  TargetedInterceptor<T> interceptor,
2917
2861
  int priority = 0
2918
2862
  )
2919
2863
  where T : ITargetedMessage
2920
2864
  {
2921
2865
  EnsureAotTargetedBridge<T>();
2922
- _ = AdvanceTick();
2866
+ long touchTick = AdvanceTick();
2923
2867
  InvalidateDispatchPlans();
2924
2868
  InterceptorCache<object> prioritizedInterceptors =
2925
2869
  _targetedInterceptsByType.GetOrAdd<T>();
@@ -2968,107 +2912,29 @@ namespace DxMessaging.Core.MessageBus
2968
2912
  );
2969
2913
 
2970
2914
  long capturedGeneration = _resetGeneration;
2971
- return () =>
2972
- {
2973
- // Generation guard: see InternalRegisterUntargeted for the
2974
- // rationale. Skip silently when the closure outlived a Reset.
2975
- if (capturedGeneration != _resetGeneration)
2976
- {
2977
- return;
2978
- }
2979
- if (
2980
- IsStaleInterceptorDeregisterAfterSweep<T>(
2981
- _targetedInterceptsByType,
2982
- capturedInterceptors
2983
- )
2984
- )
2985
- {
2986
- return;
2987
- }
2988
-
2989
- _ = AdvanceTick();
2990
- InvalidateDispatchPlans();
2991
- prioritizedInterceptors.lastTouchTicks = _tickCounter;
2992
- MarkDirtyType<T>();
2993
- _log.Log(
2994
- new MessagingRegistration(
2995
- InstanceId.EmptyId,
2996
- type,
2997
- RegistrationType.Deregister,
2998
- RegistrationMethod.Interceptor
2999
- )
3000
- );
3001
- bool removed = false;
3002
- if (_uniqueInterceptorsAndPriorities.TryGetValue(interceptor, out priorityCount))
3003
- {
3004
- if (priorityCount.TryGetValue(priority, out count))
3005
- {
3006
- if (1 < count)
3007
- {
3008
- priorityCount[priority] = count - 1;
3009
- }
3010
- else
3011
- {
3012
- removed = true;
3013
- _ = priorityCount.Remove(priority);
3014
- }
3015
- }
3016
-
3017
- if (priorityCount.Count == 0)
3018
- {
3019
- _uniqueInterceptorsAndPriorities.Remove(interceptor);
3020
- }
3021
- }
3022
- else if (MessagingDebug.enabled)
3023
- {
3024
- MessagingDebug.Log(
3025
- LogLevel.Error,
3026
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
3027
- interceptor
3028
- );
3029
- }
3030
-
3031
- bool complete = false;
3032
- if (removed)
3033
- {
3034
- if (_targetedInterceptsByType.TryGetValue<T>(out prioritizedInterceptors))
3035
- {
3036
- if (
3037
- prioritizedInterceptors.handlers.TryGetValue(
3038
- priority,
3039
- out List<object> interceptors
3040
- )
3041
- )
3042
- {
3043
- complete = interceptors.Remove(interceptor);
3044
- if (interceptors.Count == 0)
3045
- {
3046
- _ = prioritizedInterceptors.handlers.Remove(priority);
3047
- }
3048
- }
3049
- }
3050
-
3051
- if (!complete && MessagingDebug.enabled)
3052
- {
3053
- MessagingDebug.Log(
3054
- LogLevel.Error,
3055
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
3056
- interceptor
3057
- );
3058
- }
3059
- }
3060
- };
2915
+ return new MessageBusRegistration(
2916
+ MessageBusRegistration.Kind.TargetedInterceptor,
2917
+ RegistrationMethod.Interceptor,
2918
+ priority,
2919
+ capturedGeneration,
2920
+ 0L,
2921
+ touchTick,
2922
+ capturedInterceptors,
2923
+ null,
2924
+ interceptor,
2925
+ default
2926
+ );
3061
2927
  }
3062
2928
 
3063
2929
  /// <inheritdoc />
3064
- public Action RegisterBroadcastInterceptor<T>(
2930
+ public MessageBusRegistration RegisterBroadcastInterceptor<T>(
3065
2931
  BroadcastInterceptor<T> interceptor,
3066
2932
  int priority = 0
3067
2933
  )
3068
2934
  where T : IBroadcastMessage
3069
2935
  {
3070
2936
  EnsureAotSourcedBridge<T>();
3071
- _ = AdvanceTick();
2937
+ long touchTick = AdvanceTick();
3072
2938
  InvalidateDispatchPlans();
3073
2939
  InterceptorCache<object> prioritizedInterceptors =
3074
2940
  _broadcastInterceptsByType.GetOrAdd<T>();
@@ -3117,96 +2983,18 @@ namespace DxMessaging.Core.MessageBus
3117
2983
  );
3118
2984
 
3119
2985
  long capturedGeneration = _resetGeneration;
3120
- return () =>
3121
- {
3122
- // Generation guard: see InternalRegisterUntargeted for the
3123
- // rationale. Skip silently when the closure outlived a Reset.
3124
- if (capturedGeneration != _resetGeneration)
3125
- {
3126
- return;
3127
- }
3128
- if (
3129
- IsStaleInterceptorDeregisterAfterSweep<T>(
3130
- _broadcastInterceptsByType,
3131
- capturedInterceptors
3132
- )
3133
- )
3134
- {
3135
- return;
3136
- }
3137
-
3138
- _ = AdvanceTick();
3139
- InvalidateDispatchPlans();
3140
- prioritizedInterceptors.lastTouchTicks = _tickCounter;
3141
- MarkDirtyType<T>();
3142
- _log.Log(
3143
- new MessagingRegistration(
3144
- InstanceId.EmptyId,
3145
- type,
3146
- RegistrationType.Deregister,
3147
- RegistrationMethod.Interceptor
3148
- )
3149
- );
3150
- bool removed = false;
3151
- if (_uniqueInterceptorsAndPriorities.TryGetValue(interceptor, out priorityCount))
3152
- {
3153
- if (priorityCount.TryGetValue(priority, out count))
3154
- {
3155
- if (1 < count)
3156
- {
3157
- priorityCount[priority] = count - 1;
3158
- }
3159
- else
3160
- {
3161
- removed = true;
3162
- _ = priorityCount.Remove(priority);
3163
- }
3164
- }
3165
-
3166
- if (priorityCount.Count == 0)
3167
- {
3168
- _uniqueInterceptorsAndPriorities.Remove(interceptor);
3169
- }
3170
- }
3171
- else if (MessagingDebug.enabled)
3172
- {
3173
- MessagingDebug.Log(
3174
- LogLevel.Error,
3175
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
3176
- interceptor
3177
- );
3178
- }
3179
-
3180
- bool complete = false;
3181
- if (removed)
3182
- {
3183
- if (_broadcastInterceptsByType.TryGetValue<T>(out prioritizedInterceptors))
3184
- {
3185
- if (
3186
- prioritizedInterceptors.handlers.TryGetValue(
3187
- priority,
3188
- out List<object> interceptors
3189
- )
3190
- )
3191
- {
3192
- complete = interceptors.Remove(interceptor);
3193
- if (interceptors.Count == 0)
3194
- {
3195
- _ = prioritizedInterceptors.handlers.Remove(priority);
3196
- }
3197
- }
3198
- }
3199
-
3200
- if (!complete && MessagingDebug.enabled)
3201
- {
3202
- MessagingDebug.Log(
3203
- LogLevel.Error,
3204
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
3205
- interceptor
3206
- );
3207
- }
3208
- }
3209
- };
2986
+ return new MessageBusRegistration(
2987
+ MessageBusRegistration.Kind.BroadcastInterceptor,
2988
+ RegistrationMethod.Interceptor,
2989
+ priority,
2990
+ capturedGeneration,
2991
+ 0L,
2992
+ touchTick,
2993
+ capturedInterceptors,
2994
+ null,
2995
+ interceptor,
2996
+ default
2997
+ );
3210
2998
  }
3211
2999
 
3212
3000
  private bool IsStaleInterceptorDeregisterAfterSweep<T>(
@@ -3221,7 +3009,7 @@ namespace DxMessaging.Core.MessageBus
3221
3009
  }
3222
3010
 
3223
3011
  /// <inheritdoc />
3224
- public Action RegisterUntargetedPostProcessor<T>(
3012
+ public MessageBusRegistration RegisterUntargetedPostProcessor<T>(
3225
3013
  MessageHandler messageHandler,
3226
3014
  int priority = 0
3227
3015
  )
@@ -3237,7 +3025,7 @@ namespace DxMessaging.Core.MessageBus
3237
3025
  }
3238
3026
 
3239
3027
  /// <inheritdoc />
3240
- public Action RegisterTargetedPostProcessor<T>(
3028
+ public MessageBusRegistration RegisterTargetedPostProcessor<T>(
3241
3029
  InstanceId target,
3242
3030
  MessageHandler messageHandler,
3243
3031
  int priority = 0
@@ -3255,7 +3043,7 @@ namespace DxMessaging.Core.MessageBus
3255
3043
  }
3256
3044
 
3257
3045
  /// <inheritdoc />
3258
- public Action RegisterTargetedWithoutTargetingPostProcessor<T>(
3046
+ public MessageBusRegistration RegisterTargetedWithoutTargetingPostProcessor<T>(
3259
3047
  MessageHandler messageHandler,
3260
3048
  int priority = 0
3261
3049
  )
@@ -3271,7 +3059,7 @@ namespace DxMessaging.Core.MessageBus
3271
3059
  }
3272
3060
 
3273
3061
  /// <inheritdoc />
3274
- public Action RegisterBroadcastPostProcessor<T>(
3062
+ public MessageBusRegistration RegisterBroadcastPostProcessor<T>(
3275
3063
  InstanceId source,
3276
3064
  MessageHandler messageHandler,
3277
3065
  int priority = 0
@@ -3289,7 +3077,7 @@ namespace DxMessaging.Core.MessageBus
3289
3077
  }
3290
3078
 
3291
3079
  /// <inheritdoc />
3292
- public Action RegisterBroadcastWithoutSourcePostProcessor<T>(
3080
+ public MessageBusRegistration RegisterBroadcastWithoutSourcePostProcessor<T>(
3293
3081
  MessageHandler messageHandler,
3294
3082
  int priority = 0
3295
3083
  )
@@ -5122,7 +4910,7 @@ namespace DxMessaging.Core.MessageBus
5122
4910
  return true;
5123
4911
  }
5124
4912
 
5125
- private Action InternalRegisterUntargeted<T>(
4913
+ private MessageBusRegistration InternalRegisterUntargeted<T>(
5126
4914
  MessageHandler messageHandler,
5127
4915
  MessageCache<HandlerCache<int, HandlerCache>> sinks,
5128
4916
  RegistrationMethod registrationMethod,
@@ -5182,48 +4970,65 @@ namespace DxMessaging.Core.MessageBus
5182
4970
  );
5183
4971
 
5184
4972
  long capturedGeneration = _resetGeneration;
5185
- return () =>
5186
- {
5187
- // Generation guard: if ResetState() ran after this closure was
5188
- // captured (e.g. a deferred Object.Destroy fires after a
5189
- // domain-reload-style reset), silently no-op rather than
5190
- // logging a misleading over-deregistration error.
5191
- if (capturedGeneration != _resetGeneration)
5192
- {
5193
- return;
5194
- }
4973
+ return new MessageBusRegistration(
4974
+ MessageBusRegistration.Kind.Handler,
4975
+ registrationMethod,
4976
+ priority,
4977
+ capturedGeneration,
4978
+ 0L,
4979
+ touchTick,
4980
+ capturedHandlers,
4981
+ null,
4982
+ messageHandler,
4983
+ default
4984
+ );
4985
+ }
5195
4986
 
5196
- long deregisterTouchTick = AdvanceTick();
5197
- InvalidateDispatchPlans();
5198
- cache.version++;
5199
- if (
5200
- !sinks.TryGetValue<T>(out handlers)
5201
- || !ReferenceEquals(handlers, capturedHandlers)
5202
- || !handlers.handlers.TryGetValue(priority, out cache)
5203
- || !cache.handlers.TryGetValue(messageHandler, out count)
5204
- )
5205
- {
5206
- if (
5207
- capturedHandlers.handlers.Count == 0
5208
- && !ReferenceEquals(handlers, capturedHandlers)
5209
- )
5210
- {
5211
- return;
5212
- }
4987
+ /// <summary>
4988
+ /// Re-expression of the former <see cref="InternalRegisterUntargeted{T}"/> closure body:
4989
+ /// the scalar (untargeted / without-context) handler deregistration. Behaviour and the
4990
+ /// four reentrancy invariants are unchanged; the closure's captured locals are read from
4991
+ /// <paramref name="reg"/>'s fields instead, and the sink is re-resolved from the method.
4992
+ /// </summary>
4993
+ private void DeregisterScalarHandler<T>(in MessageBusRegistration reg)
4994
+ where T : IMessage
4995
+ {
4996
+ // Generation guard: if ResetState() ran after this handle was captured (e.g. a
4997
+ // deferred Object.Destroy fires after a domain-reload-style reset), silently no-op
4998
+ // rather than logging a misleading over-deregistration error.
4999
+ if (reg.generation != _resetGeneration)
5000
+ {
5001
+ return;
5002
+ }
5213
5003
 
5214
- if (MessagingDebug.enabled)
5215
- {
5216
- MessagingDebug.Log(
5217
- LogLevel.Error,
5218
- "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5219
- type,
5220
- messageHandler
5221
- );
5222
- }
5004
+ MessageHandler messageHandler = (MessageHandler)reg.payload;
5005
+ HandlerCache<int, HandlerCache> capturedHandlers =
5006
+ (HandlerCache<int, HandlerCache>)reg.capturedPrimary;
5007
+ int priority = reg.priority;
5008
+ RegistrationMethod registrationMethod = reg.method;
5009
+ SlotKey slotKey = RegistrationMethodAxes.GetSlotKey(registrationMethod);
5010
+ Type type = typeof(T);
5011
+ InstanceId handlerOwnerId = messageHandler.owner;
5223
5012
 
5224
- return;
5225
- }
5013
+ long deregisterTouchTick = AdvanceTick();
5014
+ InvalidateDispatchPlans();
5226
5015
 
5016
+ // FAST PATH: operate on the captured leaf handler-cache (already pinned on the
5017
+ // handle at registration time) DIRECTLY, without re-resolving the sink from the
5018
+ // method (ScalarSinkForMethod) or re-walking sinks->type->priority->handler. When
5019
+ // the generation guard above has passed and the captured bucket still holds this
5020
+ // handler, the captured leaf IS the live sink entry (handles are unique and never
5021
+ // reused), so the re-resolution + ReferenceEquals identity check is redundant. The
5022
+ // sweep-staleness / over-deregistration classification only matters when the handler
5023
+ // is NOT found, so it is deferred to the cold fallback below. This removes the
5024
+ // per-deregistration sink re-resolution (the measured cold-path regression source)
5025
+ // while preserving every guard and the throw-safe ordering (this method performs no
5026
+ // user callback; the throwing IMessageBus.Deregister boundary is the caller's).
5027
+ if (
5028
+ capturedHandlers.handlers.TryGetValue(priority, out HandlerCache cache)
5029
+ && cache.handlers.TryGetValue(messageHandler, out int count)
5030
+ )
5031
+ {
5227
5032
  _log.Log(
5228
5033
  new MessagingRegistration(
5229
5034
  handlerOwnerId,
@@ -5232,30 +5037,28 @@ namespace DxMessaging.Core.MessageBus
5232
5037
  registrationMethod
5233
5038
  )
5234
5039
  );
5235
- Touch(handlers, deregisterTouchTick);
5236
- handlers.version++;
5237
- handler = cache.handlers;
5040
+ Touch(capturedHandlers, deregisterTouchTick);
5041
+ capturedHandlers.version++;
5042
+ cache.version++;
5043
+ Dictionary<MessageHandler, int> handler = cache.handlers;
5238
5044
  if (count <= 1)
5239
5045
  {
5240
- bool complete = handler.Remove(messageHandler);
5241
- // List.Remove is O(n) over the same-priority bucket.
5242
- // Accepted tradeoff (here and at the context-path sibling
5243
- // site): buckets are small in practice, removal is a cold
5244
- // churn path, and the list keeps dispatch-order rebuilds
5245
- // allocation-free while preserving first-registration
5246
- // order, unlike Dictionary enumeration whose freed slots
5247
- // are reused LIFO. Mirrors the MessageHandler-side
5046
+ _ = handler.Remove(messageHandler);
5047
+ // List.Remove is O(n) over the same-priority bucket. Accepted tradeoff (here
5048
+ // and at the context-path sibling site): buckets are small in practice,
5049
+ // removal is a cold churn path, and the list keeps dispatch-order rebuilds
5050
+ // allocation-free while preserving first-registration order, unlike Dictionary
5051
+ // enumeration whose freed slots are reused LIFO. Mirrors the MessageHandler-side
5248
5052
  // insertionOrder tradeoff.
5249
5053
  _ = cache.insertionOrder.Remove(messageHandler);
5250
5054
  MarkDirtyHandler(messageHandler);
5251
- cache.version++;
5252
5055
  // do not mutate cache.cache here; let next read rebuild from handlers
5253
5056
 
5254
5057
  if (handler.Count == 0)
5255
5058
  {
5256
- _ = handlers.handlers.Remove(priority);
5059
+ _ = capturedHandlers.handlers.Remove(priority);
5257
5060
  // remove priority from order
5258
- List<int> order = handlers.order;
5061
+ List<int> order = capturedHandlers.order;
5259
5062
  int removeIdx = order.IndexOf(priority);
5260
5063
  if (removeIdx >= 0)
5261
5064
  {
@@ -5263,30 +5066,51 @@ namespace DxMessaging.Core.MessageBus
5263
5066
  }
5264
5067
  }
5265
5068
 
5266
- if (handlers.handlers.Count == 0)
5069
+ if (capturedHandlers.handlers.Count == 0)
5267
5070
  {
5268
5071
  MarkDirtyType<T>();
5269
5072
  }
5270
-
5271
- if (!complete && MessagingDebug.enabled)
5272
- {
5273
- MessagingDebug.Log(
5274
- LogLevel.Error,
5275
- "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5276
- type,
5277
- messageHandler
5278
- );
5279
- }
5280
5073
  }
5281
5074
  else
5282
5075
  {
5283
5076
  handler[messageHandler] = count - 1;
5284
5077
  }
5285
- StageDispatchSnapshot<T>(this, handlers, slotKey);
5286
- };
5078
+ StageDispatchSnapshot<T>(this, capturedHandlers, slotKey);
5079
+ return;
5080
+ }
5081
+
5082
+ // COLD FALLBACK: the handler was not found in the captured bucket, so this is a no-op
5083
+ // deregistration (post-sweep, or a genuine over-deregistration) -- nothing is removed,
5084
+ // so NO version bump / snapshot invalidation is performed. Bumping here would
5085
+ // spuriously rebuild whichever bucket currently occupies this priority, which after a
5086
+ // full deregister + re-registration at the same priority is the NEW live bucket (the
5087
+ // stale handle must not touch it). Re-resolve the live sink only to CLASSIFY this as a
5088
+ // silent stale-after-sweep no-op versus a genuine over-deregistration. Rare path; not
5089
+ // on the steady deregistration cost.
5090
+ MessageCache<HandlerCache<int, HandlerCache>> sinks = ScalarSinkForMethod(
5091
+ registrationMethod
5092
+ );
5093
+ _ = sinks.TryGetValue<T>(out HandlerCache<int, HandlerCache> handlers);
5094
+ if (
5095
+ capturedHandlers.handlers.Count == 0
5096
+ && !ReferenceEquals(handlers, capturedHandlers)
5097
+ )
5098
+ {
5099
+ return;
5100
+ }
5101
+
5102
+ if (MessagingDebug.enabled)
5103
+ {
5104
+ MessagingDebug.Log(
5105
+ LogLevel.Error,
5106
+ "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5107
+ type,
5108
+ messageHandler
5109
+ );
5110
+ }
5287
5111
  }
5288
5112
 
5289
- private Action InternalRegisterWithContext<T>(
5113
+ private MessageBusRegistration InternalRegisterWithContext<T>(
5290
5114
  InstanceId context,
5291
5115
  MessageHandler messageHandler,
5292
5116
  MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks,
@@ -5362,45 +5186,59 @@ namespace DxMessaging.Core.MessageBus
5362
5186
  StageDispatchSnapshot<T>(this, handlers, slotKey);
5363
5187
 
5364
5188
  long capturedGeneration = _resetGeneration;
5365
- return () =>
5366
- {
5367
- // Generation guard: see InternalRegisterUntargeted for the
5368
- // rationale. Skip silently when the closure outlived a Reset.
5369
- if (capturedGeneration != _resetGeneration)
5370
- {
5371
- return;
5372
- }
5189
+ return new MessageBusRegistration(
5190
+ MessageBusRegistration.Kind.Handler,
5191
+ registrationMethod,
5192
+ priority,
5193
+ capturedGeneration,
5194
+ 0L,
5195
+ touchTick,
5196
+ capturedHandlers,
5197
+ capturedBroadcastHandlers,
5198
+ messageHandler,
5199
+ context
5200
+ );
5201
+ }
5373
5202
 
5374
- long deregisterTouchTick = AdvanceTick();
5375
- InvalidateDispatchPlans();
5376
- cache.version++;
5377
- if (
5378
- !sinks.TryGetValue<T>(out broadcastHandlers)
5379
- || !ReferenceEquals(broadcastHandlers, capturedBroadcastHandlers)
5380
- || !broadcastHandlers.TryGetValue(context, out handlers)
5381
- || !ReferenceEquals(handlers, capturedHandlers)
5382
- || !handlers.handlers.TryGetValue(priority, out cache)
5383
- || !cache.handlers.TryGetValue(messageHandler, out count)
5384
- )
5385
- {
5386
- if (IsStaleContextDeregisterAfterSweep<T>(sinks, context, capturedHandlers))
5387
- {
5388
- return;
5389
- }
5203
+ /// <summary>
5204
+ /// Re-expression of the former <see cref="InternalRegisterWithContext{T}"/> closure body:
5205
+ /// the keyed (targeted / sourced-broadcast) handler deregistration. Behaviour and the four
5206
+ /// reentrancy invariants are unchanged; the closure's captured locals are read from
5207
+ /// <paramref name="reg"/>'s fields, and the sink is re-resolved from the method.
5208
+ /// </summary>
5209
+ private void DeregisterContextHandler<T>(in MessageBusRegistration reg)
5210
+ where T : IMessage
5211
+ {
5212
+ // Generation guard: see DeregisterScalarHandler for the rationale. Skip silently when
5213
+ // the handle outlived a Reset.
5214
+ if (reg.generation != _resetGeneration)
5215
+ {
5216
+ return;
5217
+ }
5390
5218
 
5391
- if (MessagingDebug.enabled)
5392
- {
5393
- MessagingDebug.Log(
5394
- LogLevel.Error,
5395
- "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5396
- type,
5397
- messageHandler
5398
- );
5399
- }
5219
+ MessageHandler messageHandler = (MessageHandler)reg.payload;
5220
+ HandlerCache<int, HandlerCache> capturedHandlers =
5221
+ (HandlerCache<int, HandlerCache>)reg.capturedPrimary;
5222
+ InstanceId context = reg.context;
5223
+ int priority = reg.priority;
5224
+ RegistrationMethod registrationMethod = reg.method;
5225
+ SlotKey slotKey = RegistrationMethodAxes.GetSlotKey(registrationMethod);
5226
+ Type type = typeof(T);
5400
5227
 
5401
- return;
5402
- }
5228
+ long deregisterTouchTick = AdvanceTick();
5229
+ InvalidateDispatchPlans();
5403
5230
 
5231
+ // FAST PATH: operate on the captured per-context leaf handler-cache directly, without
5232
+ // re-resolving the sink (ContextSinkForMethod) or re-walking sinks->type->context->
5233
+ // priority->handler. See DeregisterScalarHandler for the full rationale and the
5234
+ // sweep-staleness argument (the context sweep, like the scalar sweep, only evicts
5235
+ // EMPTY caches, so a found handler is never in a swept/detached cache). Throw-safe:
5236
+ // no user callback runs here.
5237
+ if (
5238
+ capturedHandlers.handlers.TryGetValue(priority, out HandlerCache cache)
5239
+ && cache.handlers.TryGetValue(messageHandler, out int count)
5240
+ )
5241
+ {
5404
5242
  _log.Log(
5405
5243
  new MessagingRegistration(
5406
5244
  context,
@@ -5409,23 +5247,23 @@ namespace DxMessaging.Core.MessageBus
5409
5247
  registrationMethod
5410
5248
  )
5411
5249
  );
5412
- Touch(handlers, deregisterTouchTick);
5413
- handler = cache.handlers;
5250
+ Touch(capturedHandlers, deregisterTouchTick);
5251
+ cache.version++;
5252
+ Dictionary<MessageHandler, int> handler = cache.handlers;
5414
5253
  if (count <= 1)
5415
5254
  {
5416
- bool complete = handler.Remove(messageHandler);
5417
- // O(n) List.Remove: see the tradeoff comment at the
5418
- // scalar-path sibling site in InternalRegisterUntargeted.
5255
+ _ = handler.Remove(messageHandler);
5256
+ // O(n) List.Remove: see the tradeoff comment at the scalar-path sibling site in
5257
+ // DeregisterScalarHandler.
5419
5258
  _ = cache.insertionOrder.Remove(messageHandler);
5420
5259
  MarkDirtyHandler(messageHandler);
5421
- cache.version++;
5422
5260
  // do not mutate cache.cache here; let next read rebuild from handlers
5423
5261
  if (handler.Count == 0)
5424
5262
  {
5425
- handlers.version++;
5426
- _ = handlers.handlers.Remove(priority);
5263
+ capturedHandlers.version++;
5264
+ _ = capturedHandlers.handlers.Remove(priority);
5427
5265
  // remove priority from order
5428
- List<int> order = handlers.order;
5266
+ List<int> order = capturedHandlers.order;
5429
5267
  int removeIdx = order.IndexOf(priority);
5430
5268
  if (removeIdx >= 0)
5431
5269
  {
@@ -5433,29 +5271,265 @@ namespace DxMessaging.Core.MessageBus
5433
5271
  }
5434
5272
  }
5435
5273
 
5436
- if (handlers.handlers.Count == 0)
5274
+ if (capturedHandlers.handlers.Count == 0)
5437
5275
  {
5438
5276
  MarkDirtyTarget<T>(context);
5439
5277
  }
5440
-
5441
- if (!complete && MessagingDebug.enabled)
5442
- {
5443
- MessagingDebug.Log(
5444
- LogLevel.Error,
5445
- "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5446
- type,
5447
- messageHandler
5448
- );
5449
- }
5450
5278
  }
5451
5279
  else
5452
5280
  {
5453
5281
  handler[messageHandler] = count - 1;
5454
5282
  }
5455
- StageDispatchSnapshot<T>(this, handlers, slotKey);
5283
+ StageDispatchSnapshot<T>(this, capturedHandlers, slotKey);
5284
+ return;
5285
+ }
5286
+
5287
+ // COLD FALLBACK: handler not found in the captured bucket -> a no-op deregistration
5288
+ // (nothing removed), so NO version bump is performed (it would spuriously rebuild the
5289
+ // bucket currently at this priority -- the NEW live bucket after a full deregister +
5290
+ // re-registration). Re-resolve only to CLASSIFY stale-after-sweep (silent) versus
5291
+ // over-deregistration (error). Rare path; not on the steady deregistration cost.
5292
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks =
5293
+ ContextSinkForMethod(registrationMethod);
5294
+ if (IsStaleContextDeregisterAfterSweep<T>(sinks, context, capturedHandlers))
5295
+ {
5296
+ return;
5297
+ }
5298
+
5299
+ if (MessagingDebug.enabled)
5300
+ {
5301
+ MessagingDebug.Log(
5302
+ LogLevel.Error,
5303
+ "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5304
+ type,
5305
+ messageHandler
5306
+ );
5307
+ }
5308
+ }
5309
+
5310
+ /// <inheritdoc />
5311
+ public void Deregister<T>(in MessageBusRegistration registration)
5312
+ where T : IMessage
5313
+ {
5314
+ switch (registration.kind)
5315
+ {
5316
+ case MessageBusRegistration.Kind.Handler:
5317
+ if (IsContextMethod(registration.method))
5318
+ {
5319
+ DeregisterContextHandler<T>(in registration);
5320
+ }
5321
+ else
5322
+ {
5323
+ DeregisterScalarHandler<T>(in registration);
5324
+ }
5325
+ break;
5326
+ case MessageBusRegistration.Kind.UntargetedInterceptor:
5327
+ case MessageBusRegistration.Kind.TargetedInterceptor:
5328
+ case MessageBusRegistration.Kind.BroadcastInterceptor:
5329
+ DeregisterInterceptor<T>(in registration);
5330
+ break;
5331
+ case MessageBusRegistration.Kind.GlobalAcceptAll:
5332
+ DeregisterGlobalAcceptAll(in registration);
5333
+ break;
5334
+ default:
5335
+ // Kind.None is the empty/sentinel handle (no-op); Kind.External handles are
5336
+ // minted by a foreign IMessageBus implementation and own no store here.
5337
+ break;
5338
+ }
5339
+ }
5340
+
5341
+ /// <summary>
5342
+ /// Re-expression of the former interceptor closure body. The interceptor store is selected
5343
+ /// by <see cref="MessageBusRegistration.kind"/> (all three interceptor registrars log the
5344
+ /// single <see cref="RegistrationMethod.Interceptor"/>, so the method cannot discriminate).
5345
+ /// </summary>
5346
+ private void DeregisterInterceptor<T>(in MessageBusRegistration reg)
5347
+ where T : IMessage
5348
+ {
5349
+ // Generation guard: see DeregisterScalarHandler.
5350
+ if (reg.generation != _resetGeneration)
5351
+ {
5352
+ return;
5353
+ }
5354
+
5355
+ MessageCache<InterceptorCache<object>> interceptsByType = reg.kind switch
5356
+ {
5357
+ MessageBusRegistration.Kind.UntargetedInterceptor => _untargetedInterceptsByType,
5358
+ MessageBusRegistration.Kind.TargetedInterceptor => _targetedInterceptsByType,
5359
+ MessageBusRegistration.Kind.BroadcastInterceptor => _broadcastInterceptsByType,
5360
+ _ => null,
5456
5361
  };
5362
+ if (interceptsByType == null)
5363
+ {
5364
+ return;
5365
+ }
5366
+
5367
+ InterceptorCache<object> capturedInterceptors =
5368
+ (InterceptorCache<object>)reg.capturedPrimary;
5369
+ object interceptor = reg.payload;
5370
+ int priority = reg.priority;
5371
+
5372
+ if (IsStaleInterceptorDeregisterAfterSweep<T>(interceptsByType, capturedInterceptors))
5373
+ {
5374
+ return;
5375
+ }
5376
+
5377
+ _ = AdvanceTick();
5378
+ InvalidateDispatchPlans();
5379
+ capturedInterceptors.lastTouchTicks = _tickCounter;
5380
+ MarkDirtyType<T>();
5381
+ _log.Log(
5382
+ new MessagingRegistration(
5383
+ InstanceId.EmptyId,
5384
+ typeof(T),
5385
+ RegistrationType.Deregister,
5386
+ RegistrationMethod.Interceptor
5387
+ )
5388
+ );
5389
+ bool removed = false;
5390
+ if (
5391
+ _uniqueInterceptorsAndPriorities.TryGetValue(
5392
+ interceptor,
5393
+ out Dictionary<int, int> priorityCount
5394
+ )
5395
+ )
5396
+ {
5397
+ if (priorityCount.TryGetValue(priority, out int count))
5398
+ {
5399
+ if (1 < count)
5400
+ {
5401
+ priorityCount[priority] = count - 1;
5402
+ }
5403
+ else
5404
+ {
5405
+ removed = true;
5406
+ _ = priorityCount.Remove(priority);
5407
+ }
5408
+ }
5409
+
5410
+ if (priorityCount.Count == 0)
5411
+ {
5412
+ _uniqueInterceptorsAndPriorities.Remove(interceptor);
5413
+ }
5414
+ }
5415
+ else if (MessagingDebug.enabled)
5416
+ {
5417
+ MessagingDebug.Log(
5418
+ LogLevel.Error,
5419
+ "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
5420
+ interceptor
5421
+ );
5422
+ }
5423
+
5424
+ bool complete = false;
5425
+ if (removed)
5426
+ {
5427
+ if (
5428
+ interceptsByType.TryGetValue<T>(
5429
+ out InterceptorCache<object> prioritizedInterceptors
5430
+ )
5431
+ )
5432
+ {
5433
+ if (
5434
+ prioritizedInterceptors.handlers.TryGetValue(
5435
+ priority,
5436
+ out List<object> interceptors
5437
+ )
5438
+ )
5439
+ {
5440
+ complete = interceptors.Remove(interceptor);
5441
+ if (interceptors.Count == 0)
5442
+ {
5443
+ _ = prioritizedInterceptors.handlers.Remove(priority);
5444
+ }
5445
+ }
5446
+ }
5447
+
5448
+ if (!complete && MessagingDebug.enabled)
5449
+ {
5450
+ MessagingDebug.Log(
5451
+ LogLevel.Error,
5452
+ "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
5453
+ interceptor
5454
+ );
5455
+ }
5456
+ }
5457
5457
  }
5458
5458
 
5459
+ /// <summary>
5460
+ /// True for the keyed (targeted / sourced-broadcast) handler registration methods, whose
5461
+ /// deregistration runs <see cref="DeregisterContextHandler{T}"/>; false for the scalar
5462
+ /// (untargeted / without-context) methods. Mirrors the register-side sink routing.
5463
+ /// </summary>
5464
+ private static bool IsContextMethod(RegistrationMethod method) =>
5465
+ method switch
5466
+ {
5467
+ RegistrationMethod.Targeted
5468
+ or RegistrationMethod.Broadcast
5469
+ or RegistrationMethod.TargetedPostProcessor
5470
+ or RegistrationMethod.BroadcastPostProcessor => true,
5471
+ _ => false,
5472
+ };
5473
+
5474
+ /// <summary>
5475
+ /// Reverse of the register-side hardcoded scalar-sink selection (e.g.
5476
+ /// <c>_scalarSinks[BusSinkIndex.UntargetedHandleDefault]</c>): maps a scalar handler method
5477
+ /// back to its sink so <see cref="DeregisterScalarHandler{T}"/> can re-resolve it.
5478
+ /// </summary>
5479
+ private MessageCache<HandlerCache<int, HandlerCache>> ScalarSinkForMethod(
5480
+ RegistrationMethod method
5481
+ ) =>
5482
+ method switch
5483
+ {
5484
+ RegistrationMethod.Untargeted => _scalarSinks[BusSinkIndex.UntargetedHandleDefault],
5485
+ RegistrationMethod.BroadcastWithoutSource => _scalarSinks[
5486
+ BusSinkIndex.BroadcastHandleWithoutContext
5487
+ ],
5488
+ RegistrationMethod.TargetedWithoutTargeting => _scalarSinks[
5489
+ BusSinkIndex.TargetedHandleWithoutContext
5490
+ ],
5491
+ RegistrationMethod.UntargetedPostProcessor => _scalarSinks[
5492
+ BusSinkIndex.UntargetedPostProcessDefault
5493
+ ],
5494
+ RegistrationMethod.TargetedWithoutTargetingPostProcessor => _scalarSinks[
5495
+ BusSinkIndex.TargetedPostProcessWithoutContext
5496
+ ],
5497
+ RegistrationMethod.BroadcastWithoutSourcePostProcessor => _scalarSinks[
5498
+ BusSinkIndex.BroadcastPostProcessWithoutContext
5499
+ ],
5500
+ _ => throw new ArgumentOutOfRangeException(
5501
+ nameof(method),
5502
+ method,
5503
+ "Not a scalar handler registration method."
5504
+ ),
5505
+ };
5506
+
5507
+ /// <summary>
5508
+ /// Reverse of the register-side hardcoded context-sink selection: maps a keyed handler
5509
+ /// method back to its sink so <see cref="DeregisterContextHandler{T}"/> can re-resolve it.
5510
+ /// </summary>
5511
+ private MessageCache<
5512
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
5513
+ > ContextSinkForMethod(RegistrationMethod method) =>
5514
+ method switch
5515
+ {
5516
+ RegistrationMethod.Targeted => _contextSinks[BusContextIndex.TargetedHandleDefault],
5517
+ RegistrationMethod.Broadcast => _contextSinks[
5518
+ BusContextIndex.BroadcastHandleDefault
5519
+ ],
5520
+ RegistrationMethod.TargetedPostProcessor => _contextSinks[
5521
+ BusContextIndex.TargetedPostProcessDefault
5522
+ ],
5523
+ RegistrationMethod.BroadcastPostProcessor => _contextSinks[
5524
+ BusContextIndex.BroadcastPostProcessDefault
5525
+ ],
5526
+ _ => throw new ArgumentOutOfRangeException(
5527
+ nameof(method),
5528
+ method,
5529
+ "Not a context handler registration method."
5530
+ ),
5531
+ };
5532
+
5459
5533
  private static bool IsStaleContextDeregisterAfterSweep<T>(
5460
5534
  MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks,
5461
5535
  InstanceId context,