com.wallstop-studios.dxmessaging 2.2.0 → 3.0.1

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 (248) hide show
  1. package/CHANGELOG.md +106 -67
  2. package/CHANGELOG.md.meta +7 -7
  3. package/Editor/Analyzers/BaseCallIlInspector.cs +277 -0
  4. package/Editor/Analyzers/BaseCallIlInspector.cs.meta +11 -0
  5. package/Editor/Analyzers/BaseCallLogMessageParser.cs +295 -0
  6. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +11 -0
  7. package/Editor/Analyzers/BaseCallReportAggregator.cs +308 -0
  8. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +11 -0
  9. package/Editor/Analyzers/BaseCallTypeScanner.cs +110 -0
  10. package/Editor/Analyzers/BaseCallTypeScanner.cs.meta +11 -0
  11. package/Editor/Analyzers/BaseCallTypeScannerCore.cs +562 -0
  12. package/Editor/Analyzers/BaseCallTypeScannerCore.cs.meta +11 -0
  13. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +1122 -0
  14. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +11 -0
  15. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +44 -44
  16. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +44 -44
  17. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +44 -44
  18. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +44 -44
  19. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +44 -44
  20. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  21. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +33 -0
  22. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  23. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +72 -72
  24. package/Editor/Analyzers.meta +8 -8
  25. package/Editor/AssemblyInfo.cs.meta +3 -3
  26. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +81 -0
  27. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +11 -0
  28. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +420 -0
  29. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +11 -0
  30. package/Editor/CustomEditors/MessagingComponentEditor.cs +1 -1
  31. package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +2 -2
  32. package/Editor/CustomEditors.meta +2 -2
  33. package/Editor/DxMessagingEditorInitializer.cs.meta +2 -2
  34. package/Editor/DxMessagingMenu.cs.meta +11 -11
  35. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +11 -11
  36. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +190 -0
  37. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +11 -0
  38. package/Editor/Settings/DxMessagingSettings.cs +189 -0
  39. package/Editor/Settings/DxMessagingSettings.cs.meta +2 -2
  40. package/Editor/Settings/DxMessagingSettingsProvider.cs +50 -33
  41. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +2 -2
  42. package/Editor/Settings.meta +2 -2
  43. package/Editor/SetupCscRsp.cs +209 -8
  44. package/Editor/SetupCscRsp.cs.meta +2 -2
  45. package/Editor/Testing/MessagingComponentEditorHarness.cs +1 -1
  46. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +3 -3
  47. package/Editor/Testing.meta +3 -3
  48. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef +14 -14
  49. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef.meta +7 -7
  50. package/Editor.meta +8 -8
  51. package/LICENSE.md +9 -9
  52. package/LICENSE.md.meta +7 -7
  53. package/README.md +941 -900
  54. package/README.md.meta +7 -7
  55. package/Runtime/AssemblyInfo.cs +4 -0
  56. package/Runtime/AssemblyInfo.cs.meta +2 -2
  57. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +2 -2
  58. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +2 -2
  59. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs +26 -0
  60. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +11 -0
  61. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +2 -2
  62. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +2 -2
  63. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +2 -2
  64. package/Runtime/Core/Attributes.meta +2 -2
  65. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs +195 -0
  66. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs.meta +11 -0
  67. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs +179 -0
  68. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs.meta +11 -0
  69. package/Runtime/Core/Configuration.meta +9 -0
  70. package/Runtime/Core/DataStructure/CyclicBuffer.cs +2 -2
  71. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +2 -2
  72. package/Runtime/Core/DataStructure.meta +2 -2
  73. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +2 -2
  74. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +2 -2
  75. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +2 -2
  76. package/Runtime/Core/Diagnostics.meta +2 -2
  77. package/Runtime/Core/DxMessagingStaticState.cs +19 -0
  78. package/Runtime/Core/DxMessagingStaticState.cs.meta +11 -11
  79. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +2 -2
  80. package/Runtime/Core/Extensions/IListExtensions.cs.meta +2 -2
  81. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +12 -12
  82. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +11 -11
  83. package/Runtime/Core/Extensions.meta +8 -8
  84. package/Runtime/Core/Helper/MessageCache.cs +32 -0
  85. package/Runtime/Core/Helper/MessageCache.cs.meta +2 -2
  86. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +2 -2
  87. package/Runtime/Core/Helper.meta +2 -2
  88. package/Runtime/Core/IMessage.cs +3 -3
  89. package/Runtime/Core/IMessage.cs.meta +11 -11
  90. package/Runtime/Core/InstanceId.cs.meta +11 -11
  91. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs +51 -0
  92. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs.meta +11 -0
  93. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs +38 -0
  94. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs.meta +11 -0
  95. package/Runtime/Core/Internal/TypedSlotIndex.cs +81 -0
  96. package/Runtime/Core/Internal/TypedSlotIndex.cs.meta +11 -0
  97. package/Runtime/Core/Internal/TypedSlots.cs +613 -0
  98. package/Runtime/Core/Internal/TypedSlots.cs.meta +11 -0
  99. package/Runtime/Core/Internal.meta +9 -0
  100. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +11 -11
  101. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +11 -11
  102. package/Runtime/Core/MessageBus/IMessageBus.cs +177 -3
  103. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +11 -11
  104. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +11 -11
  105. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +11 -11
  106. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs +16 -0
  107. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs.meta +11 -0
  108. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs +40 -0
  109. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs.meta +11 -0
  110. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +718 -0
  111. package/Runtime/Core/MessageBus/Internal/BusSlots.cs.meta +11 -0
  112. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs +38 -0
  113. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs.meta +11 -0
  114. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs +20 -0
  115. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs.meta +11 -0
  116. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs +28 -0
  117. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs.meta +11 -0
  118. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs +48 -0
  119. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs.meta +11 -0
  120. package/Runtime/Core/MessageBus/Internal/ISweepable.cs +15 -0
  121. package/Runtime/Core/MessageBus/Internal/ISweepable.cs.meta +11 -0
  122. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs +222 -0
  123. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs.meta +11 -0
  124. package/Runtime/Core/MessageBus/Internal/SlotKey.cs +192 -0
  125. package/Runtime/Core/MessageBus/Internal/SlotKey.cs.meta +11 -0
  126. package/Runtime/Core/MessageBus/Internal.meta +9 -0
  127. package/Runtime/Core/MessageBus/MessageBus.cs +2651 -500
  128. package/Runtime/Core/MessageBus/MessageBus.cs.meta +11 -11
  129. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +11 -11
  130. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +11 -11
  131. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +11 -11
  132. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +11 -11
  133. package/Runtime/Core/MessageBus.meta +8 -8
  134. package/Runtime/Core/MessageHandler.cs +2019 -542
  135. package/Runtime/Core/MessageHandler.cs.meta +11 -11
  136. package/Runtime/Core/MessageRegistrationHandle.cs.meta +11 -11
  137. package/Runtime/Core/MessageRegistrationToken.cs +7 -0
  138. package/Runtime/Core/MessageRegistrationToken.cs.meta +11 -11
  139. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +2 -2
  140. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +11 -11
  141. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +11 -11
  142. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +11 -11
  143. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +2 -2
  144. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +11 -11
  145. package/Runtime/Core/Messages/StringMessage.cs.meta +2 -2
  146. package/Runtime/Core/Messages.meta +8 -8
  147. package/Runtime/Core/MessagingDebug.cs.meta +11 -11
  148. package/Runtime/Core/Pooling/CollectionPool.cs +266 -0
  149. package/Runtime/Core/Pooling/CollectionPool.cs.meta +11 -0
  150. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs +30 -0
  151. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs.meta +11 -0
  152. package/Runtime/Core/Pooling/DxPools.cs +157 -0
  153. package/Runtime/Core/Pooling/DxPools.cs.meta +11 -0
  154. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs +106 -0
  155. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs.meta +11 -0
  156. package/Runtime/Core/Pooling/IDxMessagingClock.cs +18 -0
  157. package/Runtime/Core/Pooling/IDxMessagingClock.cs.meta +11 -0
  158. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs +55 -0
  159. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs.meta +11 -0
  160. package/Runtime/Core/Pooling/StopwatchClock.cs +27 -0
  161. package/Runtime/Core/Pooling/StopwatchClock.cs.meta +11 -0
  162. package/Runtime/Core/Pooling/UnityRealtimeClock.cs +31 -0
  163. package/Runtime/Core/Pooling/UnityRealtimeClock.cs.meta +11 -0
  164. package/Runtime/Core/Pooling.meta +9 -0
  165. package/Runtime/Core.meta +8 -8
  166. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +12 -12
  167. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +11 -11
  168. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +12 -12
  169. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +2 -2
  170. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs +73 -0
  171. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +11 -11
  172. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef +20 -20
  173. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef.meta +7 -7
  174. package/Runtime/Unity/Integrations/Reflex.meta +8 -8
  175. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +2 -2
  176. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs +109 -1
  177. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +11 -11
  178. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef +30 -30
  179. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef.meta +7 -7
  180. package/Runtime/Unity/Integrations/VContainer.meta +8 -8
  181. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +2 -2
  182. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef +30 -30
  183. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef.meta +7 -7
  184. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs +79 -1
  185. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +11 -11
  186. package/Runtime/Unity/Integrations/Zenject.meta +8 -8
  187. package/Runtime/Unity/Integrations.meta +8 -8
  188. package/Runtime/Unity/MessageAwareComponent.cs +29 -0
  189. package/Runtime/Unity/MessageAwareComponent.cs.meta +11 -11
  190. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +12 -12
  191. package/Runtime/Unity/MessagingComponent.cs.meta +11 -11
  192. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +12 -12
  193. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +12 -12
  194. package/Runtime/Unity.meta +8 -8
  195. package/Runtime/WallstopStudios.DxMessaging.asmdef +14 -14
  196. package/Runtime/WallstopStudios.DxMessaging.asmdef.meta +7 -7
  197. package/Runtime.meta +8 -8
  198. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab +98 -98
  199. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab.meta +7 -7
  200. package/Samples~/DI/Prefabs.meta +8 -8
  201. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset +14 -14
  202. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset.meta +8 -8
  203. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset +14 -14
  204. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset.meta +8 -8
  205. package/Samples~/DI/Providers.meta +8 -8
  206. package/Samples~/DI/README.md +51 -51
  207. package/Samples~/DI/README.md.meta +7 -7
  208. package/Samples~/DI/Reflex/SampleInstaller.cs +7 -0
  209. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +11 -11
  210. package/Samples~/DI/Reflex.meta +8 -8
  211. package/Samples~/DI/VContainer/SampleLifetimeScope.cs +6 -1
  212. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +11 -11
  213. package/Samples~/DI/VContainer.meta +8 -8
  214. package/Samples~/DI/Zenject/SampleInstaller.cs +8 -0
  215. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +11 -11
  216. package/Samples~/DI/Zenject.meta +8 -8
  217. package/Samples~/DI.meta +8 -8
  218. package/Samples~/Mini Combat/Boot.cs.meta +11 -11
  219. package/Samples~/Mini Combat/Enemy.cs.meta +11 -11
  220. package/Samples~/Mini Combat/Messages.cs.meta +11 -11
  221. package/Samples~/Mini Combat/Player.cs.meta +11 -11
  222. package/Samples~/Mini Combat/README.md +324 -323
  223. package/Samples~/Mini Combat/README.md.meta +7 -7
  224. package/Samples~/Mini Combat/UIOverlay.cs.meta +11 -11
  225. package/Samples~/Mini Combat/Walkthrough.md +430 -430
  226. package/Samples~/Mini Combat/Walkthrough.md.meta +7 -7
  227. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef +13 -13
  228. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef.meta +7 -7
  229. package/Samples~/Mini Combat.meta +8 -8
  230. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +11 -11
  231. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +11 -11
  232. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +11 -11
  233. package/Samples~/UI Buttons + Inspector/README.md +210 -209
  234. package/Samples~/UI Buttons + Inspector/README.md.meta +7 -7
  235. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +11 -11
  236. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef +13 -13
  237. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef.meta +7 -7
  238. package/Samples~/UI Buttons + Inspector.meta +8 -8
  239. package/SourceGenerators/Directory.Build.props.meta +7 -7
  240. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +11 -11
  241. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +2 -2
  242. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj.meta +7 -7
  243. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.meta +8 -8
  244. package/SourceGenerators.meta +8 -8
  245. package/Third Party Notices.md +3 -3
  246. package/Third Party Notices.md.meta +7 -7
  247. package/package.json +115 -92
  248. package/package.json.meta +7 -7
@@ -2,10 +2,13 @@ namespace DxMessaging.Core
2
2
  {
3
3
  using System;
4
4
  using System.Collections.Generic;
5
+ using System.Diagnostics;
5
6
  using System.Runtime.CompilerServices;
7
+ using DxMessaging.Core.Internal;
6
8
  using Helper;
7
9
  using MessageBus;
8
10
  using Messages;
11
+ using Pooling;
9
12
 
10
13
  /// <summary>
11
14
  /// Per-owner handler that executes registered message callbacks.
@@ -34,6 +37,76 @@ namespace DxMessaging.Core
34
37
  IComparable,
35
38
  IComparable<MessageHandler>
36
39
  {
40
+ private static void PrefreezePriorityCache<TMessage, THandler>(
41
+ TypedHandler<TMessage> handler,
42
+ int slotIndex,
43
+ int priority,
44
+ long emissionId
45
+ )
46
+ where TMessage : IMessage
47
+ {
48
+ Dictionary<int, IHandlerActionCache> byPriority = handler.GetPriorityHandlers(
49
+ slotIndex
50
+ );
51
+ if (
52
+ byPriority != null
53
+ && byPriority.TryGetValue(priority, out IHandlerActionCache erasedCache)
54
+ && erasedCache is HandlerActionCache<THandler> cache
55
+ )
56
+ {
57
+ _ = TypedHandler<TMessage>.GetOrAddNewHandlerStack(cache, emissionId);
58
+ cache.prefreezeInvocationCount++;
59
+ }
60
+ }
61
+
62
+ private static void PrefreezeContextCache<TMessage, THandler>(
63
+ TypedHandler<TMessage> handler,
64
+ int slotIndex,
65
+ InstanceId context,
66
+ int priority,
67
+ long emissionId
68
+ )
69
+ where TMessage : IMessage
70
+ {
71
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> byContext =
72
+ handler.GetContextHandlers(slotIndex);
73
+ if (
74
+ byContext != null
75
+ && byContext.TryGetValue(
76
+ context,
77
+ out Dictionary<int, IHandlerActionCache> byPriority
78
+ )
79
+ && byPriority.TryGetValue(priority, out IHandlerActionCache erasedCache)
80
+ && erasedCache is HandlerActionCache<THandler> cache
81
+ )
82
+ {
83
+ _ = TypedHandler<TMessage>.GetOrAddNewHandlerStack(cache, emissionId);
84
+ cache.prefreezeInvocationCount++;
85
+ }
86
+ }
87
+
88
+ private static int GetPriorityPrefreezeInvocationCount<TMessage, THandler>(
89
+ TypedHandler<TMessage> handler,
90
+ int slotIndex,
91
+ int priority
92
+ )
93
+ where TMessage : IMessage
94
+ {
95
+ Dictionary<int, IHandlerActionCache> byPriority = handler.GetPriorityHandlers(
96
+ slotIndex
97
+ );
98
+ if (
99
+ byPriority != null
100
+ && byPriority.TryGetValue(priority, out IHandlerActionCache erasedCache)
101
+ && erasedCache is HandlerActionCache<THandler> cache
102
+ )
103
+ {
104
+ return cache.prefreezeInvocationCount;
105
+ }
106
+
107
+ return 0;
108
+ }
109
+
37
110
  /// <summary>
38
111
  /// Pre-freezes this handler's broadcast post-processor caches for the given message type, source, and priority
39
112
  /// for the specified emission id, so registrations during the same emission are not observed.
@@ -56,32 +129,20 @@ namespace DxMessaging.Core
56
129
  return;
57
130
  }
58
131
 
59
- if (
60
- handler._broadcastPostProcessingFastHandlers != null
61
- && handler._broadcastPostProcessingFastHandlers.TryGetValue(
62
- source,
63
- out Dictionary<int, HandlerActionCache<FastHandler<T>>> fastByPriority
64
- )
65
- && fastByPriority.TryGetValue(
66
- priority,
67
- out HandlerActionCache<FastHandler<T>> fastCache
68
- )
69
- )
70
- {
71
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(fastCache, emissionId);
72
- }
73
-
74
- if (
75
- handler._broadcastPostProcessingHandlers != null
76
- && handler._broadcastPostProcessingHandlers.TryGetValue(
77
- source,
78
- out Dictionary<int, HandlerActionCache<Action<T>>> byPriority
79
- )
80
- && byPriority.TryGetValue(priority, out HandlerActionCache<Action<T>> cache)
81
- )
82
- {
83
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(cache, emissionId);
84
- }
132
+ PrefreezeContextCache<T, FastHandler<T>>(
133
+ handler,
134
+ TypedSlotIndex.BroadcastPostProcessFast,
135
+ source,
136
+ priority,
137
+ emissionId
138
+ );
139
+ PrefreezeContextCache<T, Action<T>>(
140
+ handler,
141
+ TypedSlotIndex.BroadcastPostProcessDefault,
142
+ source,
143
+ priority,
144
+ emissionId
145
+ );
85
146
  }
86
147
 
87
148
  /// <summary>
@@ -106,32 +167,20 @@ namespace DxMessaging.Core
106
167
  return;
107
168
  }
108
169
 
109
- if (
110
- handler._targetedPostProcessingFastHandlers != null
111
- && handler._targetedPostProcessingFastHandlers.TryGetValue(
112
- target,
113
- out Dictionary<int, HandlerActionCache<FastHandler<T>>> fastByPriority
114
- )
115
- && fastByPriority.TryGetValue(
116
- priority,
117
- out HandlerActionCache<FastHandler<T>> fastCache
118
- )
119
- )
120
- {
121
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(fastCache, emissionId);
122
- }
123
-
124
- if (
125
- handler._targetedPostProcessingHandlers != null
126
- && handler._targetedPostProcessingHandlers.TryGetValue(
127
- target,
128
- out Dictionary<int, HandlerActionCache<Action<T>>> byPriority
129
- )
130
- && byPriority.TryGetValue(priority, out HandlerActionCache<Action<T>> cache)
131
- )
132
- {
133
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(cache, emissionId);
134
- }
170
+ PrefreezeContextCache<T, FastHandler<T>>(
171
+ handler,
172
+ TypedSlotIndex.TargetedPostProcessFast,
173
+ target,
174
+ priority,
175
+ emissionId
176
+ );
177
+ PrefreezeContextCache<T, Action<T>>(
178
+ handler,
179
+ TypedSlotIndex.TargetedPostProcessDefault,
180
+ target,
181
+ priority,
182
+ emissionId
183
+ );
135
184
  }
136
185
 
137
186
  /// <summary>
@@ -150,27 +199,18 @@ namespace DxMessaging.Core
150
199
  return;
151
200
  }
152
201
 
153
- if (
154
- handler._fastTargetedWithoutTargetingHandlers != null
155
- && handler._fastTargetedWithoutTargetingHandlers.TryGetValue(
156
- priority,
157
- out HandlerActionCache<FastHandlerWithContext<T>> fastCache
158
- )
159
- )
160
- {
161
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(fastCache, emissionId);
162
- }
163
-
164
- if (
165
- handler._targetedWithoutTargetingHandlers != null
166
- && handler._targetedWithoutTargetingHandlers.TryGetValue(
167
- priority,
168
- out HandlerActionCache<Action<InstanceId, T>> cache
169
- )
170
- )
171
- {
172
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(cache, emissionId);
173
- }
202
+ PrefreezePriorityCache<T, FastHandlerWithContext<T>>(
203
+ handler,
204
+ TypedSlotIndex.TargetedHandleWithoutContextFast,
205
+ priority,
206
+ emissionId
207
+ );
208
+ PrefreezePriorityCache<T, Action<InstanceId, T>>(
209
+ handler,
210
+ TypedSlotIndex.TargetedHandleWithoutContext,
211
+ priority,
212
+ emissionId
213
+ );
174
214
  }
175
215
 
176
216
  /// <summary>
@@ -188,27 +228,18 @@ namespace DxMessaging.Core
188
228
  return;
189
229
  }
190
230
 
191
- if (
192
- handler._fastTargetedWithoutTargetingPostProcessingHandlers != null
193
- && handler._fastTargetedWithoutTargetingPostProcessingHandlers.TryGetValue(
194
- priority,
195
- out HandlerActionCache<FastHandlerWithContext<T>> fastCache
196
- )
197
- )
198
- {
199
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(fastCache, emissionId);
200
- }
201
-
202
- if (
203
- handler._targetedWithoutTargetingPostProcessingHandlers != null
204
- && handler._targetedWithoutTargetingPostProcessingHandlers.TryGetValue(
205
- priority,
206
- out HandlerActionCache<Action<InstanceId, T>> cache
207
- )
208
- )
209
- {
210
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(cache, emissionId);
211
- }
231
+ PrefreezePriorityCache<T, FastHandlerWithContext<T>>(
232
+ handler,
233
+ TypedSlotIndex.TargetedPostProcessWithoutContextFast,
234
+ priority,
235
+ emissionId
236
+ );
237
+ PrefreezePriorityCache<T, Action<InstanceId, T>>(
238
+ handler,
239
+ TypedSlotIndex.TargetedPostProcessWithoutContext,
240
+ priority,
241
+ emissionId
242
+ );
212
243
  }
213
244
 
214
245
  /// <summary>
@@ -226,27 +257,18 @@ namespace DxMessaging.Core
226
257
  return;
227
258
  }
228
259
 
229
- if (
230
- handler._untargetedPostProcessingFastHandlers != null
231
- && handler._untargetedPostProcessingFastHandlers.TryGetValue(
232
- priority,
233
- out HandlerActionCache<FastHandler<T>> fastCache
234
- )
235
- )
236
- {
237
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(fastCache, emissionId);
238
- }
239
-
240
- if (
241
- handler._untargetedPostProcessingHandlers != null
242
- && handler._untargetedPostProcessingHandlers.TryGetValue(
243
- priority,
244
- out HandlerActionCache<Action<T>> cache
245
- )
246
- )
247
- {
248
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(cache, emissionId);
249
- }
260
+ PrefreezePriorityCache<T, FastHandler<T>>(
261
+ handler,
262
+ TypedSlotIndex.UntargetedPostProcessFast,
263
+ priority,
264
+ emissionId
265
+ );
266
+ PrefreezePriorityCache<T, Action<T>>(
267
+ handler,
268
+ TypedSlotIndex.UntargetedPostProcessDefault,
269
+ priority,
270
+ emissionId
271
+ );
250
272
  }
251
273
 
252
274
  /// <summary>
@@ -264,27 +286,18 @@ namespace DxMessaging.Core
264
286
  return;
265
287
  }
266
288
 
267
- if (
268
- handler._fastBroadcastWithoutSourcePostProcessingHandlers != null
269
- && handler._fastBroadcastWithoutSourcePostProcessingHandlers.TryGetValue(
270
- priority,
271
- out HandlerActionCache<FastHandlerWithContext<T>> fastCache
272
- )
273
- )
274
- {
275
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(fastCache, emissionId);
276
- }
277
-
278
- if (
279
- handler._broadcastWithoutSourcePostProcessingHandlers != null
280
- && handler._broadcastWithoutSourcePostProcessingHandlers.TryGetValue(
281
- priority,
282
- out HandlerActionCache<Action<InstanceId, T>> cache
283
- )
284
- )
285
- {
286
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(cache, emissionId);
287
- }
289
+ PrefreezePriorityCache<T, FastHandlerWithContext<T>>(
290
+ handler,
291
+ TypedSlotIndex.BroadcastPostProcessWithoutContextFast,
292
+ priority,
293
+ emissionId
294
+ );
295
+ PrefreezePriorityCache<T, Action<InstanceId, T>>(
296
+ handler,
297
+ TypedSlotIndex.BroadcastPostProcessWithoutContext,
298
+ priority,
299
+ emissionId
300
+ );
288
301
  }
289
302
 
290
303
  /// <summary>
@@ -307,27 +320,128 @@ namespace DxMessaging.Core
307
320
  return;
308
321
  }
309
322
 
310
- if (
311
- handler._fastBroadcastWithoutSourceHandlers != null
312
- && handler._fastBroadcastWithoutSourceHandlers.TryGetValue(
313
- priority,
314
- out HandlerActionCache<FastHandlerWithContext<T>> fastCache
315
- )
316
- )
323
+ PrefreezePriorityCache<T, FastHandlerWithContext<T>>(
324
+ handler,
325
+ TypedSlotIndex.BroadcastHandleWithoutContextFast,
326
+ priority,
327
+ emissionId
328
+ );
329
+ PrefreezePriorityCache<T, Action<InstanceId, T>>(
330
+ handler,
331
+ TypedSlotIndex.BroadcastHandleWithoutContext,
332
+ priority,
333
+ emissionId
334
+ );
335
+ }
336
+
337
+ /// <summary>
338
+ /// Pre-freezes this handler's untargeted handler caches for the given message type and priority
339
+ /// for the specified emission id, so removals during the same emission are not observed.
340
+ /// </summary>
341
+ /// <typeparam name="T">Untargeted message type.</typeparam>
342
+ /// <param name="priority">Priority bucket to freeze.</param>
343
+ /// <param name="emissionId">Current emission id.</param>
344
+ /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
345
+ internal void PrefreezeUntargetedHandlersForEmission<T>(
346
+ int priority,
347
+ long emissionId,
348
+ IMessageBus messageBus
349
+ )
350
+ where T : IUntargetedMessage
351
+ {
352
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
317
353
  {
318
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(fastCache, emissionId);
354
+ return;
319
355
  }
320
356
 
321
- if (
322
- handler._broadcastWithoutSourceHandlers != null
323
- && handler._broadcastWithoutSourceHandlers.TryGetValue(
324
- priority,
325
- out HandlerActionCache<Action<InstanceId, T>> cache
326
- )
327
- )
357
+ PrefreezePriorityCache<T, FastHandler<T>>(
358
+ handler,
359
+ TypedSlotIndex.UntargetedHandleFast,
360
+ priority,
361
+ emissionId
362
+ );
363
+ PrefreezePriorityCache<T, Action<T>>(
364
+ handler,
365
+ TypedSlotIndex.UntargetedHandleDefault,
366
+ priority,
367
+ emissionId
368
+ );
369
+ }
370
+
371
+ /// <summary>
372
+ /// Pre-freezes this handler's targeted handler caches for the given message type, target, and priority
373
+ /// for the specified emission id, so removals during the same emission are not observed.
374
+ /// </summary>
375
+ /// <typeparam name="T">Targeted message type.</typeparam>
376
+ /// <param name="target">Target instance id.</param>
377
+ /// <param name="priority">Priority bucket to freeze.</param>
378
+ /// <param name="emissionId">Current emission id.</param>
379
+ /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
380
+ internal void PrefreezeTargetedHandlersForEmission<T>(
381
+ InstanceId target,
382
+ int priority,
383
+ long emissionId,
384
+ IMessageBus messageBus
385
+ )
386
+ where T : ITargetedMessage
387
+ {
388
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
389
+ {
390
+ return;
391
+ }
392
+
393
+ PrefreezeContextCache<T, FastHandler<T>>(
394
+ handler,
395
+ TypedSlotIndex.TargetedHandleFast,
396
+ target,
397
+ priority,
398
+ emissionId
399
+ );
400
+ PrefreezeContextCache<T, Action<T>>(
401
+ handler,
402
+ TypedSlotIndex.TargetedHandleDefault,
403
+ target,
404
+ priority,
405
+ emissionId
406
+ );
407
+ }
408
+
409
+ /// <summary>
410
+ /// Pre-freezes this handler's broadcast handler caches for the given message type, source, and priority
411
+ /// for the specified emission id, so removals during the same emission are not observed.
412
+ /// </summary>
413
+ /// <typeparam name="T">Broadcast message type.</typeparam>
414
+ /// <param name="source">Source instance id.</param>
415
+ /// <param name="priority">Priority bucket to freeze.</param>
416
+ /// <param name="emissionId">Current emission id.</param>
417
+ /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
418
+ internal void PrefreezeBroadcastHandlersForEmission<T>(
419
+ InstanceId source,
420
+ int priority,
421
+ long emissionId,
422
+ IMessageBus messageBus
423
+ )
424
+ where T : IBroadcastMessage
425
+ {
426
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
328
427
  {
329
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(cache, emissionId);
428
+ return;
330
429
  }
430
+
431
+ PrefreezeContextCache<T, FastHandler<T>>(
432
+ handler,
433
+ TypedSlotIndex.BroadcastHandleFast,
434
+ source,
435
+ priority,
436
+ emissionId
437
+ );
438
+ PrefreezeContextCache<T, Action<T>>(
439
+ handler,
440
+ TypedSlotIndex.BroadcastHandleDefault,
441
+ source,
442
+ priority,
443
+ emissionId
444
+ );
331
445
  }
332
446
 
333
447
  /// <summary>
@@ -378,6 +492,19 @@ namespace DxMessaging.Core
378
492
  ResetStatics();
379
493
  }
380
494
 
495
+ /// <summary>
496
+ /// Reclaims empty slots and pooled collections owned by the current global message bus.
497
+ /// </summary>
498
+ /// <param name="force">
499
+ /// When true, ignores idle-age thresholds and drains shared pools to zero.
500
+ /// When false, only slots past the configured idle threshold are eligible.
501
+ /// </param>
502
+ /// <returns>Counts describing what was reclaimed.</returns>
503
+ public static IMessageBus.TrimResult TrimAll(bool force = false)
504
+ {
505
+ return MessageBus.Trim(force);
506
+ }
507
+
381
508
  /// <summary>
382
509
  /// Replaces the global <see cref="Core.MessageBus.MessageBus"/> instance returned by <see cref="MessageBus"/>.
383
510
  /// </summary>
@@ -928,19 +1055,19 @@ namespace DxMessaging.Core
928
1055
  return;
929
1056
  }
930
1057
 
931
- if (handler._globalUntargetedFastHandlers != null)
1058
+ HandlerActionCache<FastHandler<IUntargetedMessage>> fastCache = handler.GetGlobalCache<
1059
+ FastHandler<IUntargetedMessage>
1060
+ >(TypedGlobalSlotIndex.UntargetedFast);
1061
+ if (fastCache != null)
932
1062
  {
933
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
934
- handler._globalUntargetedFastHandlers,
935
- emissionId
936
- );
1063
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
937
1064
  }
938
- if (handler._globalUntargetedHandlers != null)
1065
+ HandlerActionCache<Action<IUntargetedMessage>> cache = handler.GetGlobalCache<
1066
+ Action<IUntargetedMessage>
1067
+ >(TypedGlobalSlotIndex.UntargetedDefault);
1068
+ if (cache != null)
939
1069
  {
940
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
941
- handler._globalUntargetedHandlers,
942
- emissionId
943
- );
1070
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
944
1071
  }
945
1072
  }
946
1073
 
@@ -954,19 +1081,20 @@ namespace DxMessaging.Core
954
1081
  return;
955
1082
  }
956
1083
 
957
- if (handler._globalTargetedFastHandlers != null)
958
- {
959
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
960
- handler._globalTargetedFastHandlers,
961
- emissionId
1084
+ HandlerActionCache<FastHandlerWithContext<ITargetedMessage>> fastCache =
1085
+ handler.GetGlobalCache<FastHandlerWithContext<ITargetedMessage>>(
1086
+ TypedGlobalSlotIndex.TargetedFast
962
1087
  );
1088
+ if (fastCache != null)
1089
+ {
1090
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
963
1091
  }
964
- if (handler._globalTargetedHandlers != null)
1092
+ HandlerActionCache<Action<InstanceId, ITargetedMessage>> cache = handler.GetGlobalCache<
1093
+ Action<InstanceId, ITargetedMessage>
1094
+ >(TypedGlobalSlotIndex.TargetedDefault);
1095
+ if (cache != null)
965
1096
  {
966
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
967
- handler._globalTargetedHandlers,
968
- emissionId
969
- );
1097
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
970
1098
  }
971
1099
  }
972
1100
 
@@ -980,19 +1108,21 @@ namespace DxMessaging.Core
980
1108
  return;
981
1109
  }
982
1110
 
983
- if (handler._globalBroadcastFastHandlers != null)
984
- {
985
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
986
- handler._globalBroadcastFastHandlers,
987
- emissionId
1111
+ HandlerActionCache<FastHandlerWithContext<IBroadcastMessage>> fastCache =
1112
+ handler.GetGlobalCache<FastHandlerWithContext<IBroadcastMessage>>(
1113
+ TypedGlobalSlotIndex.BroadcastFast
988
1114
  );
989
- }
990
- if (handler._globalBroadcastHandlers != null)
1115
+ if (fastCache != null)
991
1116
  {
992
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
993
- handler._globalBroadcastHandlers,
994
- emissionId
1117
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
1118
+ }
1119
+ HandlerActionCache<Action<InstanceId, IBroadcastMessage>> cache =
1120
+ handler.GetGlobalCache<Action<InstanceId, IBroadcastMessage>>(
1121
+ TypedGlobalSlotIndex.BroadcastDefault
995
1122
  );
1123
+ if (cache != null)
1124
+ {
1125
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
996
1126
  }
997
1127
  }
998
1128
 
@@ -1071,25 +1201,28 @@ namespace DxMessaging.Core
1071
1201
  Action untargetedDeregistration = typedHandler.AddGlobalUntargetedHandler(
1072
1202
  originalUntargetedMessageHandler,
1073
1203
  untargetedMessageHandler,
1074
- NullDeregistration
1204
+ NullDeregistration,
1205
+ messageBus
1075
1206
  );
1076
1207
  Action targetedDeregistration = typedHandler.AddGlobalTargetedHandler(
1077
1208
  originalTargetedMessageHandler,
1078
1209
  targetedMessageHandler,
1079
- NullDeregistration
1210
+ NullDeregistration,
1211
+ messageBus
1080
1212
  );
1081
1213
  Action broadcastDeregistration = typedHandler.AddGlobalBroadcastHandler(
1082
1214
  originalBroadcastMessageHandler,
1083
1215
  broadcastMessageHandler,
1084
- NullDeregistration
1216
+ NullDeregistration,
1217
+ messageBus
1085
1218
  );
1086
1219
 
1087
1220
  return () =>
1088
1221
  {
1222
+ messageBusDeregistration?.Invoke();
1089
1223
  untargetedDeregistration();
1090
1224
  targetedDeregistration();
1091
1225
  broadcastDeregistration();
1092
- messageBusDeregistration?.Invoke();
1093
1226
  };
1094
1227
 
1095
1228
  void NullDeregistration()
@@ -1123,25 +1256,28 @@ namespace DxMessaging.Core
1123
1256
  Action untargetedDeregistration = typedHandler.AddGlobalUntargetedHandler(
1124
1257
  originalUntargetedMessageHandler,
1125
1258
  untargetedMessageHandler,
1126
- NullDeregistration
1259
+ NullDeregistration,
1260
+ messageBus
1127
1261
  );
1128
1262
  Action targetedDeregistration = typedHandler.AddGlobalTargetedHandler(
1129
1263
  originalTargetedMessageHandler,
1130
1264
  targetedMessageHandler,
1131
- NullDeregistration
1265
+ NullDeregistration,
1266
+ messageBus
1132
1267
  );
1133
1268
  Action broadcastDeregistration = typedHandler.AddGlobalBroadcastHandler(
1134
1269
  originalBroadcastMessageHandler,
1135
1270
  broadcastMessageHandler,
1136
- NullDeregistration
1271
+ NullDeregistration,
1272
+ messageBus
1137
1273
  );
1138
1274
 
1139
1275
  return () =>
1140
1276
  {
1277
+ messageBusDeregistration?.Invoke();
1141
1278
  untargetedDeregistration();
1142
1279
  targetedDeregistration();
1143
1280
  broadcastDeregistration();
1144
- messageBusDeregistration?.Invoke();
1145
1281
  };
1146
1282
 
1147
1283
  void NullDeregistration()
@@ -1181,7 +1317,7 @@ namespace DxMessaging.Core
1181
1317
  messageHandler,
1182
1318
  messageBusDeregistration,
1183
1319
  priority,
1184
- messageBus.EmissionId
1320
+ messageBus
1185
1321
  );
1186
1322
  }
1187
1323
 
@@ -1216,7 +1352,7 @@ namespace DxMessaging.Core
1216
1352
  messageHandler,
1217
1353
  messageBusDeregistration,
1218
1354
  priority,
1219
- messageBus.EmissionId
1355
+ messageBus
1220
1356
  );
1221
1357
  }
1222
1358
 
@@ -1251,7 +1387,7 @@ namespace DxMessaging.Core
1251
1387
  messageHandler,
1252
1388
  messageBusDeregistration,
1253
1389
  priority,
1254
- messageBus.EmissionId
1390
+ messageBus
1255
1391
  );
1256
1392
  }
1257
1393
 
@@ -1286,7 +1422,7 @@ namespace DxMessaging.Core
1286
1422
  messageHandler,
1287
1423
  messageBusDeregistration,
1288
1424
  priority,
1289
- messageBus.EmissionId
1425
+ messageBus
1290
1426
  );
1291
1427
  }
1292
1428
 
@@ -1318,7 +1454,7 @@ namespace DxMessaging.Core
1318
1454
  messageHandler,
1319
1455
  messageBusDeregistration,
1320
1456
  priority,
1321
- messageBus.EmissionId
1457
+ messageBus
1322
1458
  );
1323
1459
  }
1324
1460
 
@@ -1350,7 +1486,7 @@ namespace DxMessaging.Core
1350
1486
  messageHandler,
1351
1487
  messageBusDeregistration,
1352
1488
  priority,
1353
- messageBus.EmissionId
1489
+ messageBus
1354
1490
  );
1355
1491
  }
1356
1492
 
@@ -1381,7 +1517,7 @@ namespace DxMessaging.Core
1381
1517
  messageHandler,
1382
1518
  messageBusDeregistration,
1383
1519
  priority,
1384
- messageBus.EmissionId
1520
+ messageBus
1385
1521
  );
1386
1522
  }
1387
1523
 
@@ -1412,7 +1548,7 @@ namespace DxMessaging.Core
1412
1548
  messageHandler,
1413
1549
  messageBusDeregistration,
1414
1550
  priority,
1415
- messageBus.EmissionId
1551
+ messageBus
1416
1552
  );
1417
1553
  }
1418
1554
 
@@ -1443,7 +1579,7 @@ namespace DxMessaging.Core
1443
1579
  messageHandler,
1444
1580
  messageBusDeregistration,
1445
1581
  priority,
1446
- messageBus.EmissionId
1582
+ messageBus
1447
1583
  );
1448
1584
  }
1449
1585
 
@@ -1474,7 +1610,7 @@ namespace DxMessaging.Core
1474
1610
  messageHandler,
1475
1611
  messageBusDeregistration,
1476
1612
  priority,
1477
- messageBus.EmissionId
1613
+ messageBus
1478
1614
  );
1479
1615
  }
1480
1616
 
@@ -1505,7 +1641,7 @@ namespace DxMessaging.Core
1505
1641
  messageHandler,
1506
1642
  messageBusDeregistration,
1507
1643
  priority,
1508
- messageBus.EmissionId
1644
+ messageBus
1509
1645
  );
1510
1646
  }
1511
1647
 
@@ -1536,7 +1672,7 @@ namespace DxMessaging.Core
1536
1672
  messageHandler,
1537
1673
  messageBusDeregistration,
1538
1674
  priority,
1539
- messageBus.EmissionId
1675
+ messageBus
1540
1676
  );
1541
1677
  }
1542
1678
 
@@ -1572,7 +1708,7 @@ namespace DxMessaging.Core
1572
1708
  messageHandler,
1573
1709
  messageBusDeregistration,
1574
1710
  priority,
1575
- messageBus.EmissionId
1711
+ messageBus
1576
1712
  );
1577
1713
  }
1578
1714
 
@@ -1607,7 +1743,7 @@ namespace DxMessaging.Core
1607
1743
  messageHandler,
1608
1744
  messageBusDeregistration,
1609
1745
  priority,
1610
- messageBus.EmissionId
1746
+ messageBus
1611
1747
  );
1612
1748
  }
1613
1749
 
@@ -1638,7 +1774,7 @@ namespace DxMessaging.Core
1638
1774
  messageHandler,
1639
1775
  messageBusDeregistration,
1640
1776
  priority,
1641
- messageBus.EmissionId
1777
+ messageBus
1642
1778
  );
1643
1779
  }
1644
1780
 
@@ -1669,7 +1805,7 @@ namespace DxMessaging.Core
1669
1805
  messageHandler,
1670
1806
  messageBusDeregistration,
1671
1807
  priority,
1672
- messageBus.EmissionId
1808
+ messageBus
1673
1809
  );
1674
1810
  }
1675
1811
 
@@ -1704,7 +1840,7 @@ namespace DxMessaging.Core
1704
1840
  messageHandler,
1705
1841
  messageBusDeregistration,
1706
1842
  priority,
1707
- messageBus.EmissionId
1843
+ messageBus
1708
1844
  );
1709
1845
  }
1710
1846
 
@@ -1739,7 +1875,7 @@ namespace DxMessaging.Core
1739
1875
  messageHandler,
1740
1876
  messageBusDeregistration,
1741
1877
  priority,
1742
- messageBus.EmissionId
1878
+ messageBus
1743
1879
  );
1744
1880
  }
1745
1881
 
@@ -1771,7 +1907,7 @@ namespace DxMessaging.Core
1771
1907
  messageHandler,
1772
1908
  messageBusDeregistration,
1773
1909
  priority,
1774
- messageBus.EmissionId
1910
+ messageBus
1775
1911
  );
1776
1912
  }
1777
1913
 
@@ -1803,7 +1939,7 @@ namespace DxMessaging.Core
1803
1939
  messageHandler,
1804
1940
  messageBusDeregistration,
1805
1941
  priority,
1806
- messageBus.EmissionId
1942
+ messageBus
1807
1943
  );
1808
1944
  }
1809
1945
 
@@ -1996,31 +2132,120 @@ namespace DxMessaging.Core
1996
2132
  return false;
1997
2133
  }
1998
2134
 
2135
+ /// <summary>
2136
+ /// Resets empty typed-handler slots associated with
2137
+ /// <paramref name="messageBus"/>. The eviction layer calls through
2138
+ /// this erased surface after bus-side slots prove idle and empty.
2139
+ /// </summary>
2140
+ /// <param name="messageBus">
2141
+ /// Bus whose typed-handler cache should be swept. Null resolves to
2142
+ /// this handler's default bus.
2143
+ /// </param>
2144
+ /// <returns>Number of typed or typed-global slots reset.</returns>
2145
+ internal int ResetEmptyTypedSlotsForSweep(IMessageBus messageBus = null)
2146
+ {
2147
+ messageBus = ResolveMessageBus(messageBus);
2148
+ int messageBusIndex = messageBus.RegisteredGlobalSequentialIndex;
2149
+ if (messageBusIndex < 0 || _handlersByTypeByMessageBus.Count <= messageBusIndex)
2150
+ {
2151
+ return 0;
2152
+ }
2153
+
2154
+ int resetCount = 0;
2155
+ MessageCache<object> handlersByType = _handlersByTypeByMessageBus[messageBusIndex];
2156
+ foreach (object untypedHandler in _handlersByTypeByMessageBus[messageBusIndex])
2157
+ {
2158
+ if (untypedHandler is ITypedHandlerSlotSweeper sweeper)
2159
+ {
2160
+ resetCount += sweeper.ResetEmptySlotsForSweep();
2161
+ if (sweeper.MarkedForOuterRemoval)
2162
+ {
2163
+ handlersByType.RemoveAtIndex(sweeper.MessageTypeIndex);
2164
+ }
2165
+ }
2166
+ }
2167
+
2168
+ return resetCount;
2169
+ }
2170
+
2171
+ internal int ResetAllTypedSlotsForBusReset(IMessageBus messageBus = null)
2172
+ {
2173
+ messageBus = ResolveMessageBus(messageBus);
2174
+ int messageBusIndex = messageBus.RegisteredGlobalSequentialIndex;
2175
+ if (messageBusIndex < 0 || _handlersByTypeByMessageBus.Count <= messageBusIndex)
2176
+ {
2177
+ return 0;
2178
+ }
2179
+
2180
+ int resetCount = 0;
2181
+ foreach (object untypedHandler in _handlersByTypeByMessageBus[messageBusIndex])
2182
+ {
2183
+ if (untypedHandler is ITypedHandlerSlotSweeper sweeper)
2184
+ {
2185
+ resetCount += sweeper.ResetAllSlotsForBusReset();
2186
+ }
2187
+ }
2188
+
2189
+ return resetCount;
2190
+ }
2191
+
2192
+ internal int CountEmptyTypedSlotsForSweep(IMessageBus messageBus = null)
2193
+ {
2194
+ messageBus = ResolveMessageBus(messageBus);
2195
+ int messageBusIndex = messageBus.RegisteredGlobalSequentialIndex;
2196
+ if (messageBusIndex < 0 || _handlersByTypeByMessageBus.Count <= messageBusIndex)
2197
+ {
2198
+ return 0;
2199
+ }
2200
+
2201
+ int count = 0;
2202
+ foreach (object untypedHandler in _handlersByTypeByMessageBus[messageBusIndex])
2203
+ {
2204
+ if (untypedHandler is ITypedHandlerSlotSweeper sweeper)
2205
+ {
2206
+ count += sweeper.CountEmptySlotsForSweep();
2207
+ }
2208
+ }
2209
+
2210
+ return count;
2211
+ }
2212
+
2213
+ internal bool HasTypedHandlersForBus(IMessageBus messageBus = null)
2214
+ {
2215
+ messageBus = ResolveMessageBus(messageBus);
2216
+ int messageBusIndex = messageBus.RegisteredGlobalSequentialIndex;
2217
+ if (messageBusIndex < 0 || _handlersByTypeByMessageBus.Count <= messageBusIndex)
2218
+ {
2219
+ return false;
2220
+ }
2221
+
2222
+ foreach (object untypedHandler in _handlersByTypeByMessageBus[messageBusIndex])
2223
+ {
2224
+ if (untypedHandler != null)
2225
+ {
2226
+ return true;
2227
+ }
2228
+ }
2229
+
2230
+ return false;
2231
+ }
2232
+
1999
2233
  internal int GetUntargetedPostProcessingPrefreezeCount<T>(
2000
2234
  IMessageBus messageBus,
2001
2235
  int priority
2002
2236
  )
2003
2237
  where T : IMessage
2004
2238
  {
2005
- if (
2006
- !GetHandlerForType(messageBus, out TypedHandler<T> handler)
2007
- || handler._untargetedPostProcessingFastHandlers == null
2008
- )
2239
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
2009
2240
  {
2010
2241
  return 0;
2011
2242
  }
2012
2243
 
2013
- if (
2014
- handler._untargetedPostProcessingFastHandlers.TryGetValue(
2015
- priority,
2016
- out HandlerActionCache<FastHandler<T>> cache
2017
- )
2018
- )
2019
- {
2020
- return cache.prefreezeInvocationCount;
2021
- }
2022
-
2023
- return 0;
2244
+ return GetPriorityPrefreezeInvocationCount<T, FastHandler<T>>(
2245
+ handler,
2246
+ TypedSlotIndex.UntargetedPostProcessFast,
2247
+ priority
2248
+ );
2024
2249
  }
2025
2250
 
2026
2251
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2121,8 +2346,12 @@ namespace DxMessaging.Core
2121
2346
  return typedHandler.GetOrCreateBroadcastWithoutSourcePostLink();
2122
2347
  }
2123
2348
 
2124
- internal sealed class HandlerActionCache<T>
2349
+ internal sealed class HandlerActionCache<T> : DxMessaging.Core.Internal.IHandlerActionCache
2125
2350
  {
2351
+ // Uses outer T as a field type -- reflection callers must close
2352
+ // via MakeGenericType(outer.GetGenericArguments()) before passing
2353
+ // this type to Activator.CreateInstance. See
2354
+ // Tests/Editor/Contract/ReflectionHelpers.cs::CloseNestedGeneric.
2126
2355
  internal readonly struct Entry
2127
2356
  {
2128
2357
  /// <summary>
@@ -2144,18 +2373,76 @@ namespace DxMessaging.Core
2144
2373
  public readonly List<T> cache = new();
2145
2374
  public long version;
2146
2375
  public long lastSeenVersion = -1;
2147
- public long lastSeenEmissionId;
2376
+ public long lastSeenEmissionId = -1;
2148
2377
  internal int prefreezeInvocationCount;
2378
+
2379
+ /// <summary>Monotonic version field, read-only on the interface surface.</summary>
2380
+ long DxMessaging.Core.Internal.IHandlerActionCache.Version
2381
+ {
2382
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2383
+ get => version;
2384
+ }
2385
+
2386
+ /// <summary>Most recent dispatcher-observed version; mutable through the staged dispatch path.</summary>
2387
+ long DxMessaging.Core.Internal.IHandlerActionCache.LastSeenVersion
2388
+ {
2389
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2390
+ get => lastSeenVersion;
2391
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2392
+ set => lastSeenVersion = value;
2393
+ }
2394
+
2395
+ /// <summary>Most recent dispatcher-observed bus emission id.</summary>
2396
+ long DxMessaging.Core.Internal.IHandlerActionCache.LastSeenEmissionId
2397
+ {
2398
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2399
+ get => lastSeenEmissionId;
2400
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2401
+ set => lastSeenEmissionId = value;
2402
+ }
2403
+
2404
+ /// <summary>Prefreeze invocation counter mirror; maintained by the dispatchers.</summary>
2405
+ int DxMessaging.Core.Internal.IHandlerActionCache.PrefreezeInvocationCount
2406
+ {
2407
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2408
+ get => prefreezeInvocationCount;
2409
+ }
2410
+
2411
+ /// <summary>True iff the entries dictionary holds zero handlers.</summary>
2412
+ bool DxMessaging.Core.Internal.IHandlerActionCache.IsEmpty
2413
+ {
2414
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2415
+ get => entries.Count == 0;
2416
+ }
2417
+
2418
+ /// <summary>
2419
+ /// Eviction-driven full clear; bumps <see cref="version"/> as the LAST step
2420
+ /// so captured dispatch closures observe invalidation.
2421
+ /// </summary>
2422
+ void DxMessaging.Core.Internal.IHandlerActionCache.Reset()
2423
+ {
2424
+ entries.Clear();
2425
+ cache.Clear();
2426
+ lastSeenVersion = -1;
2427
+ lastSeenEmissionId = -1;
2428
+ prefreezeInvocationCount = 0;
2429
+ unchecked
2430
+ {
2431
+ ++version;
2432
+ }
2433
+ }
2149
2434
  }
2150
2435
 
2151
2436
  internal sealed class UntargetedDispatchLink<T>
2152
2437
  where T : IMessage
2153
2438
  {
2154
2439
  private readonly TypedHandler<T> typedHandler;
2440
+ internal readonly long capturedGeneration;
2155
2441
 
2156
- internal UntargetedDispatchLink(TypedHandler<T> typedHandler)
2442
+ internal UntargetedDispatchLink(TypedHandler<T> typedHandler, long capturedGeneration)
2157
2443
  {
2158
2444
  this.typedHandler = typedHandler;
2445
+ this.capturedGeneration = capturedGeneration;
2159
2446
  }
2160
2447
 
2161
2448
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2166,6 +2453,14 @@ namespace DxMessaging.Core
2166
2453
  long emissionId
2167
2454
  )
2168
2455
  {
2456
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2457
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2458
+ // walks when the outer wrapper has been reclaimed.
2459
+ if (typedHandler._outerGeneration != capturedGeneration)
2460
+ {
2461
+ return;
2462
+ }
2463
+
2169
2464
  if (!messageHandler.active)
2170
2465
  {
2171
2466
  return;
@@ -2179,10 +2474,15 @@ namespace DxMessaging.Core
2179
2474
  where TMessage : IMessage
2180
2475
  {
2181
2476
  private readonly TypedHandler<TMessage> typedHandler;
2477
+ internal readonly long capturedGeneration;
2182
2478
 
2183
- internal UntargetedPostDispatchLink(TypedHandler<TMessage> typedHandler)
2479
+ internal UntargetedPostDispatchLink(
2480
+ TypedHandler<TMessage> typedHandler,
2481
+ long capturedGeneration
2482
+ )
2184
2483
  {
2185
2484
  this.typedHandler = typedHandler;
2485
+ this.capturedGeneration = capturedGeneration;
2186
2486
  }
2187
2487
 
2188
2488
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2193,6 +2493,14 @@ namespace DxMessaging.Core
2193
2493
  long emissionId
2194
2494
  )
2195
2495
  {
2496
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2497
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2498
+ // walks when the outer wrapper has been reclaimed.
2499
+ if (typedHandler._outerGeneration != capturedGeneration)
2500
+ {
2501
+ return;
2502
+ }
2503
+
2196
2504
  if (!messageHandler.active)
2197
2505
  {
2198
2506
  return;
@@ -2206,10 +2514,15 @@ namespace DxMessaging.Core
2206
2514
  where TMessage : IMessage
2207
2515
  {
2208
2516
  private readonly TypedHandler<TMessage> typedHandler;
2517
+ internal readonly long capturedGeneration;
2209
2518
 
2210
- internal TargetedDispatchLink(TypedHandler<TMessage> typedHandler)
2519
+ internal TargetedDispatchLink(
2520
+ TypedHandler<TMessage> typedHandler,
2521
+ long capturedGeneration
2522
+ )
2211
2523
  {
2212
2524
  this.typedHandler = typedHandler;
2525
+ this.capturedGeneration = capturedGeneration;
2213
2526
  }
2214
2527
 
2215
2528
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2221,6 +2534,14 @@ namespace DxMessaging.Core
2221
2534
  long emissionId
2222
2535
  )
2223
2536
  {
2537
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2538
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2539
+ // walks when the outer wrapper has been reclaimed.
2540
+ if (typedHandler._outerGeneration != capturedGeneration)
2541
+ {
2542
+ return;
2543
+ }
2544
+
2224
2545
  typedHandler.HandleTargeted(ref target, ref message, priority, emissionId);
2225
2546
  }
2226
2547
  }
@@ -2229,10 +2550,15 @@ namespace DxMessaging.Core
2229
2550
  where TMessage : IMessage
2230
2551
  {
2231
2552
  private readonly TypedHandler<TMessage> typedHandler;
2553
+ internal readonly long capturedGeneration;
2232
2554
 
2233
- internal TargetedPostDispatchLink(TypedHandler<TMessage> typedHandler)
2555
+ internal TargetedPostDispatchLink(
2556
+ TypedHandler<TMessage> typedHandler,
2557
+ long capturedGeneration
2558
+ )
2234
2559
  {
2235
2560
  this.typedHandler = typedHandler;
2561
+ this.capturedGeneration = capturedGeneration;
2236
2562
  }
2237
2563
 
2238
2564
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2244,6 +2570,14 @@ namespace DxMessaging.Core
2244
2570
  long emissionId
2245
2571
  )
2246
2572
  {
2573
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2574
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2575
+ // walks when the outer wrapper has been reclaimed.
2576
+ if (typedHandler._outerGeneration != capturedGeneration)
2577
+ {
2578
+ return;
2579
+ }
2580
+
2247
2581
  typedHandler.HandleTargetedPostProcessing(
2248
2582
  ref target,
2249
2583
  ref message,
@@ -2257,10 +2591,15 @@ namespace DxMessaging.Core
2257
2591
  where TMessage : IMessage
2258
2592
  {
2259
2593
  private readonly TypedHandler<TMessage> typedHandler;
2594
+ internal readonly long capturedGeneration;
2260
2595
 
2261
- internal TargetedWithoutTargetingDispatchLink(TypedHandler<TMessage> typedHandler)
2596
+ internal TargetedWithoutTargetingDispatchLink(
2597
+ TypedHandler<TMessage> typedHandler,
2598
+ long capturedGeneration
2599
+ )
2262
2600
  {
2263
2601
  this.typedHandler = typedHandler;
2602
+ this.capturedGeneration = capturedGeneration;
2264
2603
  }
2265
2604
 
2266
2605
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2272,6 +2611,14 @@ namespace DxMessaging.Core
2272
2611
  long emissionId
2273
2612
  )
2274
2613
  {
2614
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2615
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2616
+ // walks when the outer wrapper has been reclaimed.
2617
+ if (typedHandler._outerGeneration != capturedGeneration)
2618
+ {
2619
+ return;
2620
+ }
2621
+
2275
2622
  typedHandler.HandleTargetedWithoutTargeting(
2276
2623
  ref target,
2277
2624
  ref message,
@@ -2285,10 +2632,15 @@ namespace DxMessaging.Core
2285
2632
  where TMessage : IMessage
2286
2633
  {
2287
2634
  private readonly TypedHandler<TMessage> typedHandler;
2635
+ internal readonly long capturedGeneration;
2288
2636
 
2289
- internal TargetedWithoutTargetingPostDispatchLink(TypedHandler<TMessage> typedHandler)
2637
+ internal TargetedWithoutTargetingPostDispatchLink(
2638
+ TypedHandler<TMessage> typedHandler,
2639
+ long capturedGeneration
2640
+ )
2290
2641
  {
2291
2642
  this.typedHandler = typedHandler;
2643
+ this.capturedGeneration = capturedGeneration;
2292
2644
  }
2293
2645
 
2294
2646
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2300,6 +2652,14 @@ namespace DxMessaging.Core
2300
2652
  long emissionId
2301
2653
  )
2302
2654
  {
2655
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2656
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2657
+ // walks when the outer wrapper has been reclaimed.
2658
+ if (typedHandler._outerGeneration != capturedGeneration)
2659
+ {
2660
+ return;
2661
+ }
2662
+
2303
2663
  typedHandler.HandleTargetedWithoutTargetingPostProcessing(
2304
2664
  ref target,
2305
2665
  ref message,
@@ -2313,10 +2673,15 @@ namespace DxMessaging.Core
2313
2673
  where TMessage : IMessage
2314
2674
  {
2315
2675
  private readonly TypedHandler<TMessage> typedHandler;
2676
+ internal readonly long capturedGeneration;
2316
2677
 
2317
- internal BroadcastDispatchLink(TypedHandler<TMessage> typedHandler)
2678
+ internal BroadcastDispatchLink(
2679
+ TypedHandler<TMessage> typedHandler,
2680
+ long capturedGeneration
2681
+ )
2318
2682
  {
2319
2683
  this.typedHandler = typedHandler;
2684
+ this.capturedGeneration = capturedGeneration;
2320
2685
  }
2321
2686
 
2322
2687
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2328,6 +2693,14 @@ namespace DxMessaging.Core
2328
2693
  long emissionId
2329
2694
  )
2330
2695
  {
2696
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2697
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2698
+ // walks when the outer wrapper has been reclaimed.
2699
+ if (typedHandler._outerGeneration != capturedGeneration)
2700
+ {
2701
+ return;
2702
+ }
2703
+
2331
2704
  typedHandler.HandleSourcedBroadcast(ref source, ref message, priority, emissionId);
2332
2705
  }
2333
2706
  }
@@ -2336,10 +2709,15 @@ namespace DxMessaging.Core
2336
2709
  where TMessage : IMessage
2337
2710
  {
2338
2711
  private readonly TypedHandler<TMessage> typedHandler;
2712
+ internal readonly long capturedGeneration;
2339
2713
 
2340
- internal BroadcastPostDispatchLink(TypedHandler<TMessage> typedHandler)
2714
+ internal BroadcastPostDispatchLink(
2715
+ TypedHandler<TMessage> typedHandler,
2716
+ long capturedGeneration
2717
+ )
2341
2718
  {
2342
2719
  this.typedHandler = typedHandler;
2720
+ this.capturedGeneration = capturedGeneration;
2343
2721
  }
2344
2722
 
2345
2723
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2351,6 +2729,14 @@ namespace DxMessaging.Core
2351
2729
  long emissionId
2352
2730
  )
2353
2731
  {
2732
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2733
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2734
+ // walks when the outer wrapper has been reclaimed.
2735
+ if (typedHandler._outerGeneration != capturedGeneration)
2736
+ {
2737
+ return;
2738
+ }
2739
+
2354
2740
  typedHandler.HandleSourcedBroadcastPostProcessing(
2355
2741
  ref source,
2356
2742
  ref message,
@@ -2364,10 +2750,15 @@ namespace DxMessaging.Core
2364
2750
  where TMessage : IMessage
2365
2751
  {
2366
2752
  private readonly TypedHandler<TMessage> typedHandler;
2753
+ internal readonly long capturedGeneration;
2367
2754
 
2368
- internal BroadcastWithoutSourceDispatchLink(TypedHandler<TMessage> typedHandler)
2755
+ internal BroadcastWithoutSourceDispatchLink(
2756
+ TypedHandler<TMessage> typedHandler,
2757
+ long capturedGeneration
2758
+ )
2369
2759
  {
2370
2760
  this.typedHandler = typedHandler;
2761
+ this.capturedGeneration = capturedGeneration;
2371
2762
  }
2372
2763
 
2373
2764
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2379,6 +2770,14 @@ namespace DxMessaging.Core
2379
2770
  long emissionId
2380
2771
  )
2381
2772
  {
2773
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2774
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2775
+ // walks when the outer wrapper has been reclaimed.
2776
+ if (typedHandler._outerGeneration != capturedGeneration)
2777
+ {
2778
+ return;
2779
+ }
2780
+
2382
2781
  typedHandler.HandleSourcedBroadcastWithoutSource(
2383
2782
  ref source,
2384
2783
  ref message,
@@ -2392,10 +2791,15 @@ namespace DxMessaging.Core
2392
2791
  where TMessage : IMessage
2393
2792
  {
2394
2793
  private readonly TypedHandler<TMessage> typedHandler;
2794
+ internal readonly long capturedGeneration;
2395
2795
 
2396
- internal BroadcastWithoutSourcePostDispatchLink(TypedHandler<TMessage> typedHandler)
2796
+ internal BroadcastWithoutSourcePostDispatchLink(
2797
+ TypedHandler<TMessage> typedHandler,
2798
+ long capturedGeneration
2799
+ )
2397
2800
  {
2398
2801
  this.typedHandler = typedHandler;
2802
+ this.capturedGeneration = capturedGeneration;
2399
2803
  }
2400
2804
 
2401
2805
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2407,6 +2811,14 @@ namespace DxMessaging.Core
2407
2811
  long emissionId
2408
2812
  )
2409
2813
  {
2814
+ // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2815
+ // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2816
+ // walks when the outer wrapper has been reclaimed.
2817
+ if (typedHandler._outerGeneration != capturedGeneration)
2818
+ {
2819
+ return;
2820
+ }
2821
+
2410
2822
  typedHandler.HandleBroadcastWithoutSourcePostProcessing(
2411
2823
  ref source,
2412
2824
  ref message,
@@ -2420,220 +2832,447 @@ namespace DxMessaging.Core
2420
2832
  /// One-size-fits-all wrapper around all possible Messaging sinks for a particular MessageHandler & MessageType.
2421
2833
  /// </summary>
2422
2834
  /// <typeparam name="T">Message type that this Handler exists to serve.</typeparam>
2423
- internal sealed class TypedHandler<T>
2835
+ internal sealed class TypedHandler<T> : ITypedHandlerSlotSweeper
2424
2836
  where T : IMessage
2425
2837
  {
2426
- internal Dictionary<
2427
- InstanceId,
2428
- Dictionary<int, HandlerActionCache<Action<T>>>
2429
- > _targetedHandlers;
2430
- internal Dictionary<int, HandlerActionCache<Action<T>>> _untargetedHandlers;
2431
- internal Dictionary<
2432
- InstanceId,
2433
- Dictionary<int, HandlerActionCache<Action<T>>>
2434
- > _broadcastHandlers;
2435
- internal Dictionary<
2436
- InstanceId,
2437
- Dictionary<int, HandlerActionCache<Action<T>>>
2438
- > _targetedPostProcessingHandlers;
2439
- internal Dictionary<
2440
- int,
2441
- HandlerActionCache<Action<T>>
2442
- > _untargetedPostProcessingHandlers;
2443
- internal Dictionary<
2444
- InstanceId,
2445
- Dictionary<int, HandlerActionCache<Action<T>>>
2446
- > _broadcastPostProcessingHandlers;
2447
- internal Dictionary<
2448
- InstanceId,
2449
- Dictionary<int, HandlerActionCache<FastHandler<T>>>
2450
- > _targetedFastHandlers;
2451
- internal Dictionary<int, HandlerActionCache<FastHandler<T>>> _untargetedFastHandlers;
2452
- internal Dictionary<
2453
- InstanceId,
2454
- Dictionary<int, HandlerActionCache<FastHandler<T>>>
2455
- > _broadcastFastHandlers;
2456
- internal Dictionary<
2457
- InstanceId,
2458
- Dictionary<int, HandlerActionCache<FastHandler<T>>>
2459
- > _targetedPostProcessingFastHandlers;
2460
- internal Dictionary<
2461
- int,
2462
- HandlerActionCache<FastHandler<T>>
2463
- > _untargetedPostProcessingFastHandlers;
2464
- internal Dictionary<
2465
- InstanceId,
2466
- Dictionary<int, HandlerActionCache<FastHandler<T>>>
2467
- > _broadcastPostProcessingFastHandlers;
2468
-
2469
- internal HandlerActionCache<Action<IUntargetedMessage>> _globalUntargetedHandlers;
2470
-
2471
- internal HandlerActionCache<
2472
- Action<InstanceId, ITargetedMessage>
2473
- > _globalTargetedHandlers;
2838
+ // Typed storage: 20 typed slots + 6 global slots + 10 dispatch
2839
+ // links. The legacy named fields were deleted so new handler
2840
+ // variants must pick an explicit axis-indexed slot.
2841
+ internal readonly TypedSlot<T>[] _slots = new TypedSlot<T>[TypedSlotIndex.Length];
2842
+ internal readonly TypedGlobalSlot[] _globalSlots = new TypedGlobalSlot[
2843
+ TypedGlobalSlotIndex.Length
2844
+ ];
2845
+ internal readonly object[] _dispatchLinks = new object[TypedDispatchLinkIndex.Length];
2474
2846
 
2475
- internal HandlerActionCache<
2476
- Action<InstanceId, IBroadcastMessage>
2477
- > _globalBroadcastHandlers;
2847
+ // Constructor exists solely so the [Conditional("DEBUG")]
2848
+ // validator below runs at construction time. In Release builds
2849
+ // the Conditional attribute strips the call site, leaving an
2850
+ // empty constructor body that the JIT collapses to the
2851
+ // equivalent of the implicit default. Mirrors the
2852
+ // MessageBus.ValidateSinkArrays() pattern.
2853
+ internal TypedHandler()
2854
+ {
2855
+ ValidateSlotArrays();
2856
+ }
2478
2857
 
2479
- internal HandlerActionCache<
2480
- FastHandler<IUntargetedMessage>
2481
- > _globalUntargetedFastHandlers;
2858
+ internal long _outerGeneration;
2859
+ internal bool _markedForOuterRemoval;
2482
2860
 
2483
- internal HandlerActionCache<
2484
- FastHandlerWithContext<ITargetedMessage>
2485
- > _globalTargetedFastHandlers;
2861
+ int ITypedHandlerSlotSweeper.MessageTypeIndex
2862
+ {
2863
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2864
+ get => MessageHelperIndexer<T>.SequentialId;
2865
+ }
2486
2866
 
2487
- internal HandlerActionCache<
2488
- FastHandlerWithContext<IBroadcastMessage>
2489
- > _globalBroadcastFastHandlers;
2490
- internal Dictionary<
2491
- int,
2492
- HandlerActionCache<Action<InstanceId, T>>
2493
- > _targetedWithoutTargetingHandlers;
2494
- internal Dictionary<
2495
- int,
2496
- HandlerActionCache<FastHandlerWithContext<T>>
2497
- > _fastTargetedWithoutTargetingHandlers;
2498
- internal Dictionary<
2499
- int,
2500
- HandlerActionCache<Action<InstanceId, T>>
2501
- > _broadcastWithoutSourceHandlers;
2502
- internal Dictionary<
2503
- int,
2504
- HandlerActionCache<FastHandlerWithContext<T>>
2505
- > _fastBroadcastWithoutSourceHandlers;
2506
- internal Dictionary<
2507
- int,
2508
- HandlerActionCache<Action<InstanceId, T>>
2509
- > _targetedWithoutTargetingPostProcessingHandlers;
2510
- internal Dictionary<
2511
- int,
2512
- HandlerActionCache<FastHandlerWithContext<T>>
2513
- > _fastTargetedWithoutTargetingPostProcessingHandlers;
2514
- internal Dictionary<
2515
- int,
2516
- HandlerActionCache<Action<InstanceId, T>>
2517
- > _broadcastWithoutSourcePostProcessingHandlers;
2518
- internal Dictionary<
2519
- int,
2520
- HandlerActionCache<FastHandlerWithContext<T>>
2521
- > _fastBroadcastWithoutSourcePostProcessingHandlers;
2522
- private UntargetedDispatchLink<T> _untargetedLink;
2523
- private object _untargetedPostLink;
2524
- private object _targetedLink;
2525
- private object _targetedPostLink;
2526
- private object _targetedWithoutTargetingLink;
2527
- private object _targetedWithoutTargetingPostLink;
2528
- private object _broadcastLink;
2529
- private object _broadcastPostLink;
2530
- private object _broadcastWithoutSourceLink;
2531
- private object _broadcastWithoutSourcePostLink;
2867
+ bool ITypedHandlerSlotSweeper.MarkedForOuterRemoval
2868
+ {
2869
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2870
+ get => _markedForOuterRemoval;
2871
+ }
2532
2872
 
2533
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2534
- internal UntargetedDispatchLink<T> GetOrCreateUntargetedLink()
2873
+ [Conditional("DEBUG")]
2874
+ private void ValidateSlotArrays()
2535
2875
  {
2536
- UntargetedDispatchLink<T> link = _untargetedLink;
2537
- if (link == null)
2876
+ if (_slots.Length != TypedSlotIndex.Length)
2538
2877
  {
2539
- link = new UntargetedDispatchLink<T>(this);
2540
- _untargetedLink = link;
2878
+ throw new InvalidOperationException(
2879
+ $"_slots length is {_slots.Length} but TypedSlotIndex.Length is {TypedSlotIndex.Length}."
2880
+ );
2881
+ }
2882
+ if (_globalSlots.Length != TypedGlobalSlotIndex.Length)
2883
+ {
2884
+ throw new InvalidOperationException(
2885
+ $"_globalSlots length is {_globalSlots.Length} but TypedGlobalSlotIndex.Length is {TypedGlobalSlotIndex.Length}."
2886
+ );
2887
+ }
2888
+ if (_dispatchLinks.Length != TypedDispatchLinkIndex.Length)
2889
+ {
2890
+ throw new InvalidOperationException(
2891
+ $"_dispatchLinks length is {_dispatchLinks.Length} but TypedDispatchLinkIndex.Length is {TypedDispatchLinkIndex.Length}."
2892
+ );
2893
+ }
2894
+ // Lazy registration writers update the slot arrays; this assertion still
2895
+ // holds at construction (slots populate on first register,
2896
+ // not on construction). The invariant flips meaning -- not
2897
+ // the message -- when writers land.
2898
+ for (int i = 0; i < _slots.Length; ++i)
2899
+ {
2900
+ if (_slots[i] != null)
2901
+ {
2902
+ throw new InvalidOperationException(
2903
+ $"_slots[{i}] is non-null at construction; expected null per TypedSlotIndex because slots populate lazily on first registration."
2904
+ );
2905
+ }
2906
+ }
2907
+ for (int i = 0; i < _globalSlots.Length; ++i)
2908
+ {
2909
+ if (_globalSlots[i] != null)
2910
+ {
2911
+ throw new InvalidOperationException(
2912
+ $"_globalSlots[{i}] is non-null at construction; expected null per TypedGlobalSlotIndex because slots populate lazily on first registration."
2913
+ );
2914
+ }
2915
+ }
2916
+ for (int i = 0; i < _dispatchLinks.Length; ++i)
2917
+ {
2918
+ if (_dispatchLinks[i] != null)
2919
+ {
2920
+ throw new InvalidOperationException(
2921
+ $"_dispatchLinks[{i}] is non-null at construction; expected null per TypedDispatchLinkIndex because links populate lazily on first dispatch-link request."
2922
+ );
2923
+ }
2541
2924
  }
2542
-
2543
- return link;
2544
2925
  }
2545
2926
 
2546
2927
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2547
- internal UntargetedPostDispatchLink<T> GetOrCreateUntargetedPostLink()
2928
+ private TypedSlot<T> GetOrCreateSlot(int index, bool requiresContext)
2548
2929
  {
2549
- UntargetedPostDispatchLink<T> link =
2550
- _untargetedPostLink as UntargetedPostDispatchLink<T>;
2551
- if (link == null)
2930
+ TypedSlot<T> slot = _slots[index];
2931
+ if (slot == null)
2552
2932
  {
2553
- link = new UntargetedPostDispatchLink<T>(this);
2554
- _untargetedPostLink = link;
2933
+ slot = new TypedSlot<T>(requiresContext);
2934
+ _slots[index] = slot;
2555
2935
  }
2556
2936
 
2557
- return link;
2937
+ return slot;
2558
2938
  }
2559
2939
 
2560
2940
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2561
- internal TargetedDispatchLink<T> GetOrCreateTargetedLink()
2941
+ internal Dictionary<int, IHandlerActionCache> GetOrCreatePriorityHandlers(
2942
+ int index,
2943
+ bool requiresContext
2944
+ )
2562
2945
  {
2563
- TargetedDispatchLink<T> link = _targetedLink as TargetedDispatchLink<T>;
2564
- if (link == null)
2565
- {
2566
- link = new TargetedDispatchLink<T>(this);
2567
- _targetedLink = link;
2568
- }
2946
+ return GetOrCreateSlot(index, requiresContext).byPriority;
2947
+ }
2569
2948
 
2570
- return link;
2949
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2950
+ internal Dictionary<int, IHandlerActionCache> GetPriorityHandlers(int index)
2951
+ {
2952
+ return _slots[index]?.byPriority;
2571
2953
  }
2572
2954
 
2573
2955
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2574
- internal TargetedPostDispatchLink<T> GetOrCreateTargetedPostLink()
2956
+ internal Dictionary<
2957
+ InstanceId,
2958
+ Dictionary<int, IHandlerActionCache>
2959
+ > GetOrCreateContextHandlers(int index)
2575
2960
  {
2576
- TargetedPostDispatchLink<T> link = _targetedPostLink as TargetedPostDispatchLink<T>;
2577
- if (link == null)
2578
- {
2579
- link = new TargetedPostDispatchLink<T>(this);
2580
- _targetedPostLink = link;
2581
- }
2961
+ TypedSlot<T> slot = GetOrCreateSlot(index, requiresContext: true);
2962
+ slot.byContext ??= DxPools.TypedHandlerContextDicts.Rent();
2963
+ return slot.byContext;
2964
+ }
2582
2965
 
2583
- return link;
2966
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2967
+ internal Dictionary<
2968
+ InstanceId,
2969
+ Dictionary<int, IHandlerActionCache>
2970
+ > GetContextHandlers(int index)
2971
+ {
2972
+ return _slots[index]?.byContext;
2584
2973
  }
2585
2974
 
2586
2975
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2587
- internal TargetedWithoutTargetingDispatchLink<T> GetOrCreateTargetedWithoutTargetingLink()
2976
+ internal TypedGlobalSlot GetOrCreateGlobalSlot(int index)
2588
2977
  {
2589
- TargetedWithoutTargetingDispatchLink<T> link =
2590
- _targetedWithoutTargetingLink as TargetedWithoutTargetingDispatchLink<T>;
2591
- if (link == null)
2978
+ TypedGlobalSlot slot = _globalSlots[index];
2979
+ if (slot == null)
2592
2980
  {
2593
- link = new TargetedWithoutTargetingDispatchLink<T>(this);
2594
- _targetedWithoutTargetingLink = link;
2981
+ slot = new TypedGlobalSlot();
2982
+ _globalSlots[index] = slot;
2595
2983
  }
2596
2984
 
2597
- return link;
2985
+ return slot;
2598
2986
  }
2599
2987
 
2600
2988
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2601
- internal TargetedWithoutTargetingPostDispatchLink<T> GetOrCreateTargetedWithoutTargetingPostLink()
2989
+ internal HandlerActionCache<TU> GetGlobalCache<TU>(int index)
2602
2990
  {
2603
- TargetedWithoutTargetingPostDispatchLink<T> link =
2604
- _targetedWithoutTargetingPostLink
2605
- as TargetedWithoutTargetingPostDispatchLink<T>;
2606
- if (link == null)
2607
- {
2608
- link = new TargetedWithoutTargetingPostDispatchLink<T>(this);
2609
- _targetedWithoutTargetingPostLink = link;
2610
- }
2611
-
2612
- return link;
2991
+ return _globalSlots[index]?.cache as HandlerActionCache<TU>;
2613
2992
  }
2614
2993
 
2615
2994
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2616
- internal BroadcastDispatchLink<T> GetOrCreateBroadcastLink()
2995
+ private TypedSlot<T> FindPrioritySlot(Dictionary<int, IHandlerActionCache> handlers)
2617
2996
  {
2618
- BroadcastDispatchLink<T> link = _broadcastLink as BroadcastDispatchLink<T>;
2619
- if (link == null)
2997
+ for (int i = 0; i < _slots.Length; ++i)
2620
2998
  {
2621
- link = new BroadcastDispatchLink<T>(this);
2622
- _broadcastLink = link;
2999
+ TypedSlot<T> slot = _slots[i];
3000
+ if (slot != null && ReferenceEquals(slot.byPriority, handlers))
3001
+ {
3002
+ return slot;
3003
+ }
2623
3004
  }
2624
3005
 
2625
- return link;
3006
+ return null;
2626
3007
  }
2627
3008
 
2628
3009
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2629
- internal BroadcastPostDispatchLink<T> GetOrCreateBroadcastPostLink()
3010
+ private TypedSlot<T> FindContextSlot(
3011
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext
3012
+ )
2630
3013
  {
2631
- BroadcastPostDispatchLink<T> link =
2632
- _broadcastPostLink as BroadcastPostDispatchLink<T>;
3014
+ for (int i = 0; i < _slots.Length; ++i)
3015
+ {
3016
+ TypedSlot<T> slot = _slots[i];
3017
+ if (slot != null && ReferenceEquals(slot.byContext, handlersByContext))
3018
+ {
3019
+ return slot;
3020
+ }
3021
+ }
3022
+
3023
+ return null;
3024
+ }
3025
+
3026
+ int ITypedHandlerSlotSweeper.ResetEmptySlotsForSweep()
3027
+ {
3028
+ _markedForOuterRemoval = false;
3029
+ int resetCount = 0;
3030
+ for (int i = 0; i < _slots.Length; ++i)
3031
+ {
3032
+ TypedSlot<T> slot = _slots[i];
3033
+ if (slot != null && slot.IsEmpty)
3034
+ {
3035
+ slot.Reset();
3036
+ _slots[i] = null;
3037
+ resetCount++;
3038
+ }
3039
+ }
3040
+
3041
+ for (int i = 0; i < _globalSlots.Length; ++i)
3042
+ {
3043
+ TypedGlobalSlot slot = _globalSlots[i];
3044
+ if (slot != null && slot.IsEmpty)
3045
+ {
3046
+ slot.Reset();
3047
+ _globalSlots[i] = null;
3048
+ resetCount++;
3049
+ }
3050
+ }
3051
+
3052
+ MarkForOuterRemovalIfEmpty();
3053
+ return resetCount;
3054
+ }
3055
+
3056
+ int ITypedHandlerSlotSweeper.ResetAllSlotsForBusReset()
3057
+ {
3058
+ _markedForOuterRemoval = false;
3059
+ int resetCount = 0;
3060
+ for (int i = 0; i < _slots.Length; ++i)
3061
+ {
3062
+ TypedSlot<T> slot = _slots[i];
3063
+ if (slot != null)
3064
+ {
3065
+ slot.Reset();
3066
+ _slots[i] = null;
3067
+ resetCount++;
3068
+ }
3069
+ }
3070
+
3071
+ for (int i = 0; i < _globalSlots.Length; ++i)
3072
+ {
3073
+ TypedGlobalSlot slot = _globalSlots[i];
3074
+ if (slot != null)
3075
+ {
3076
+ slot.Reset();
3077
+ _globalSlots[i] = null;
3078
+ resetCount++;
3079
+ }
3080
+ }
3081
+
3082
+ ClearDispatchLinks();
3083
+ unchecked
3084
+ {
3085
+ ++_outerGeneration;
3086
+ }
3087
+ return resetCount;
3088
+ }
3089
+
3090
+ int ITypedHandlerSlotSweeper.CountEmptySlotsForSweep()
3091
+ {
3092
+ int count = 0;
3093
+ for (int i = 0; i < _slots.Length; ++i)
3094
+ {
3095
+ TypedSlot<T> slot = _slots[i];
3096
+ if (slot != null && slot.IsEmpty)
3097
+ {
3098
+ count++;
3099
+ }
3100
+ }
3101
+
3102
+ for (int i = 0; i < _globalSlots.Length; ++i)
3103
+ {
3104
+ TypedGlobalSlot slot = _globalSlots[i];
3105
+ if (slot != null && slot.IsEmpty)
3106
+ {
3107
+ count++;
3108
+ }
3109
+ }
3110
+
3111
+ return count;
3112
+ }
3113
+
3114
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3115
+ private void MarkForOuterRemovalIfEmpty()
3116
+ {
3117
+ if (HasLiveSlots())
3118
+ {
3119
+ return;
3120
+ }
3121
+
3122
+ ClearDispatchLinks();
3123
+ _markedForOuterRemoval = true;
3124
+ unchecked
3125
+ {
3126
+ ++_outerGeneration;
3127
+ }
3128
+ }
3129
+
3130
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3131
+ private bool HasLiveSlots()
3132
+ {
3133
+ for (int i = 0; i < _slots.Length; ++i)
3134
+ {
3135
+ if (_slots[i] != null)
3136
+ {
3137
+ return true;
3138
+ }
3139
+ }
3140
+
3141
+ for (int i = 0; i < _globalSlots.Length; ++i)
3142
+ {
3143
+ if (_globalSlots[i] != null)
3144
+ {
3145
+ return true;
3146
+ }
3147
+ }
3148
+
3149
+ return false;
3150
+ }
3151
+
3152
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3153
+ private void ClearDispatchLinks()
3154
+ {
3155
+ for (int i = 0; i < _dispatchLinks.Length; ++i)
3156
+ {
3157
+ _dispatchLinks[i] = null;
3158
+ }
3159
+ }
3160
+
3161
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3162
+ internal UntargetedDispatchLink<T> GetOrCreateUntargetedLink()
3163
+ {
3164
+ UntargetedDispatchLink<T> link =
3165
+ _dispatchLinks[TypedDispatchLinkIndex.UntargetedHandle]
3166
+ as UntargetedDispatchLink<T>;
3167
+ if (link == null)
3168
+ {
3169
+ link = new UntargetedDispatchLink<T>(this, _outerGeneration);
3170
+ _dispatchLinks[TypedDispatchLinkIndex.UntargetedHandle] = link;
3171
+ }
3172
+
3173
+ return link;
3174
+ }
3175
+
3176
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3177
+ internal UntargetedPostDispatchLink<T> GetOrCreateUntargetedPostLink()
3178
+ {
3179
+ UntargetedPostDispatchLink<T> link =
3180
+ _dispatchLinks[TypedDispatchLinkIndex.UntargetedPostProcess]
3181
+ as UntargetedPostDispatchLink<T>;
3182
+ if (link == null)
3183
+ {
3184
+ link = new UntargetedPostDispatchLink<T>(this, _outerGeneration);
3185
+ _dispatchLinks[TypedDispatchLinkIndex.UntargetedPostProcess] = link;
3186
+ }
3187
+
3188
+ return link;
3189
+ }
3190
+
3191
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3192
+ internal TargetedDispatchLink<T> GetOrCreateTargetedLink()
3193
+ {
3194
+ TargetedDispatchLink<T> link =
3195
+ _dispatchLinks[TypedDispatchLinkIndex.TargetedHandle]
3196
+ as TargetedDispatchLink<T>;
3197
+ if (link == null)
3198
+ {
3199
+ link = new TargetedDispatchLink<T>(this, _outerGeneration);
3200
+ _dispatchLinks[TypedDispatchLinkIndex.TargetedHandle] = link;
3201
+ }
3202
+
3203
+ return link;
3204
+ }
3205
+
3206
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3207
+ internal TargetedPostDispatchLink<T> GetOrCreateTargetedPostLink()
3208
+ {
3209
+ TargetedPostDispatchLink<T> link =
3210
+ _dispatchLinks[TypedDispatchLinkIndex.TargetedPostProcess]
3211
+ as TargetedPostDispatchLink<T>;
3212
+ if (link == null)
3213
+ {
3214
+ link = new TargetedPostDispatchLink<T>(this, _outerGeneration);
3215
+ _dispatchLinks[TypedDispatchLinkIndex.TargetedPostProcess] = link;
3216
+ }
3217
+
3218
+ return link;
3219
+ }
3220
+
3221
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3222
+ internal TargetedWithoutTargetingDispatchLink<T> GetOrCreateTargetedWithoutTargetingLink()
3223
+ {
3224
+ TargetedWithoutTargetingDispatchLink<T> link =
3225
+ _dispatchLinks[TypedDispatchLinkIndex.TargetedHandleWithoutContext]
3226
+ as TargetedWithoutTargetingDispatchLink<T>;
3227
+ if (link == null)
3228
+ {
3229
+ link = new TargetedWithoutTargetingDispatchLink<T>(this, _outerGeneration);
3230
+ _dispatchLinks[TypedDispatchLinkIndex.TargetedHandleWithoutContext] = link;
3231
+ }
3232
+
3233
+ return link;
3234
+ }
3235
+
3236
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3237
+ internal TargetedWithoutTargetingPostDispatchLink<T> GetOrCreateTargetedWithoutTargetingPostLink()
3238
+ {
3239
+ TargetedWithoutTargetingPostDispatchLink<T> link =
3240
+ _dispatchLinks[TypedDispatchLinkIndex.TargetedPostProcessWithoutContext]
3241
+ as TargetedWithoutTargetingPostDispatchLink<T>;
3242
+ if (link == null)
3243
+ {
3244
+ link = new TargetedWithoutTargetingPostDispatchLink<T>(this, _outerGeneration);
3245
+ _dispatchLinks[TypedDispatchLinkIndex.TargetedPostProcessWithoutContext] = link;
3246
+ }
3247
+
3248
+ return link;
3249
+ }
3250
+
3251
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3252
+ internal BroadcastDispatchLink<T> GetOrCreateBroadcastLink()
3253
+ {
3254
+ BroadcastDispatchLink<T> link =
3255
+ _dispatchLinks[TypedDispatchLinkIndex.BroadcastHandle]
3256
+ as BroadcastDispatchLink<T>;
3257
+ if (link == null)
3258
+ {
3259
+ link = new BroadcastDispatchLink<T>(this, _outerGeneration);
3260
+ _dispatchLinks[TypedDispatchLinkIndex.BroadcastHandle] = link;
3261
+ }
3262
+
3263
+ return link;
3264
+ }
3265
+
3266
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3267
+ internal BroadcastPostDispatchLink<T> GetOrCreateBroadcastPostLink()
3268
+ {
3269
+ BroadcastPostDispatchLink<T> link =
3270
+ _dispatchLinks[TypedDispatchLinkIndex.BroadcastPostProcess]
3271
+ as BroadcastPostDispatchLink<T>;
2633
3272
  if (link == null)
2634
3273
  {
2635
- link = new BroadcastPostDispatchLink<T>(this);
2636
- _broadcastPostLink = link;
3274
+ link = new BroadcastPostDispatchLink<T>(this, _outerGeneration);
3275
+ _dispatchLinks[TypedDispatchLinkIndex.BroadcastPostProcess] = link;
2637
3276
  }
2638
3277
 
2639
3278
  return link;
@@ -2643,11 +3282,12 @@ namespace DxMessaging.Core
2643
3282
  internal BroadcastWithoutSourceDispatchLink<T> GetOrCreateBroadcastWithoutSourceLink()
2644
3283
  {
2645
3284
  BroadcastWithoutSourceDispatchLink<T> link =
2646
- _broadcastWithoutSourceLink as BroadcastWithoutSourceDispatchLink<T>;
3285
+ _dispatchLinks[TypedDispatchLinkIndex.BroadcastHandleWithoutContext]
3286
+ as BroadcastWithoutSourceDispatchLink<T>;
2647
3287
  if (link == null)
2648
3288
  {
2649
- link = new BroadcastWithoutSourceDispatchLink<T>(this);
2650
- _broadcastWithoutSourceLink = link;
3289
+ link = new BroadcastWithoutSourceDispatchLink<T>(this, _outerGeneration);
3290
+ _dispatchLinks[TypedDispatchLinkIndex.BroadcastHandleWithoutContext] = link;
2651
3291
  }
2652
3292
 
2653
3293
  return link;
@@ -2657,11 +3297,13 @@ namespace DxMessaging.Core
2657
3297
  internal BroadcastWithoutSourcePostDispatchLink<T> GetOrCreateBroadcastWithoutSourcePostLink()
2658
3298
  {
2659
3299
  BroadcastWithoutSourcePostDispatchLink<T> link =
2660
- _broadcastWithoutSourcePostLink as BroadcastWithoutSourcePostDispatchLink<T>;
3300
+ _dispatchLinks[TypedDispatchLinkIndex.BroadcastPostProcessWithoutContext]
3301
+ as BroadcastWithoutSourcePostDispatchLink<T>;
2661
3302
  if (link == null)
2662
3303
  {
2663
- link = new BroadcastWithoutSourcePostDispatchLink<T>(this);
2664
- _broadcastWithoutSourcePostLink = link;
3304
+ link = new BroadcastWithoutSourcePostDispatchLink<T>(this, _outerGeneration);
3305
+ _dispatchLinks[TypedDispatchLinkIndex.BroadcastPostProcessWithoutContext] =
3306
+ link;
2665
3307
  }
2666
3308
 
2667
3309
  return link;
@@ -2674,19 +3316,18 @@ namespace DxMessaging.Core
2674
3316
  /// <param name="priority">Priority at which to run the handlers.</param>
2675
3317
  public void HandleUntargeted(ref T message, int priority, long emissionId)
2676
3318
  {
2677
- PrefreezeHandlersForEmission(
2678
- _untargetedPostProcessingFastHandlers,
3319
+ RunFastHandlers(
3320
+ GetPriorityHandlers(TypedSlotIndex.UntargetedHandleFast),
3321
+ ref message,
2679
3322
  priority,
2680
3323
  emissionId
2681
3324
  );
2682
- PrefreezeHandlersForEmission(
2683
- _untargetedPostProcessingHandlers,
3325
+ RunHandlers(
3326
+ GetPriorityHandlers(TypedSlotIndex.UntargetedHandleDefault),
3327
+ ref message,
2684
3328
  priority,
2685
3329
  emissionId
2686
3330
  );
2687
-
2688
- RunFastHandlers(_untargetedFastHandlers, ref message, priority, emissionId);
2689
- RunHandlers(_untargetedHandlers, ref message, priority, emissionId);
2690
3331
  }
2691
3332
 
2692
3333
  /// <summary>
@@ -2704,14 +3345,14 @@ namespace DxMessaging.Core
2704
3345
  {
2705
3346
  RunFastHandlersWithContext(
2706
3347
  ref target,
2707
- _targetedFastHandlers,
3348
+ GetContextHandlers(TypedSlotIndex.TargetedHandleFast),
2708
3349
  ref message,
2709
3350
  priority,
2710
3351
  emissionId
2711
3352
  );
2712
3353
  RunHandlersWithContext(
2713
3354
  ref target,
2714
- _targetedHandlers,
3355
+ GetContextHandlers(TypedSlotIndex.TargetedHandleDefault),
2715
3356
  ref message,
2716
3357
  priority,
2717
3358
  emissionId
@@ -2733,14 +3374,14 @@ namespace DxMessaging.Core
2733
3374
  {
2734
3375
  RunFastHandlers(
2735
3376
  ref target,
2736
- _fastTargetedWithoutTargetingHandlers,
3377
+ GetPriorityHandlers(TypedSlotIndex.TargetedHandleWithoutContextFast),
2737
3378
  ref message,
2738
3379
  priority,
2739
3380
  emissionId
2740
3381
  );
2741
3382
  RunHandlers(
2742
3383
  ref target,
2743
- _targetedWithoutTargetingHandlers,
3384
+ GetPriorityHandlers(TypedSlotIndex.TargetedHandleWithoutContext),
2744
3385
  ref message,
2745
3386
  priority,
2746
3387
  emissionId
@@ -2762,14 +3403,14 @@ namespace DxMessaging.Core
2762
3403
  {
2763
3404
  RunFastHandlersWithContext(
2764
3405
  ref source,
2765
- _broadcastFastHandlers,
3406
+ GetContextHandlers(TypedSlotIndex.BroadcastHandleFast),
2766
3407
  ref message,
2767
3408
  priority,
2768
3409
  emissionId
2769
3410
  );
2770
3411
  RunHandlersWithContext(
2771
3412
  ref source,
2772
- _broadcastHandlers,
3413
+ GetContextHandlers(TypedSlotIndex.BroadcastHandleDefault),
2773
3414
  ref message,
2774
3415
  priority,
2775
3416
  emissionId
@@ -2791,14 +3432,14 @@ namespace DxMessaging.Core
2791
3432
  {
2792
3433
  RunFastHandlers(
2793
3434
  ref source,
2794
- _fastBroadcastWithoutSourceHandlers,
3435
+ GetPriorityHandlers(TypedSlotIndex.BroadcastHandleWithoutContextFast),
2795
3436
  ref message,
2796
3437
  priority,
2797
3438
  emissionId
2798
3439
  );
2799
3440
  RunHandlers(
2800
3441
  ref source,
2801
- _broadcastWithoutSourceHandlers,
3442
+ GetPriorityHandlers(TypedSlotIndex.BroadcastHandleWithoutContext),
2802
3443
  ref message,
2803
3444
  priority,
2804
3445
  emissionId
@@ -2811,14 +3452,32 @@ namespace DxMessaging.Core
2811
3452
  /// <param name="message">Message to emit.</param>
2812
3453
  public void HandleGlobalUntargeted(ref IUntargetedMessage message, long emissionId)
2813
3454
  {
2814
- RunFastHandlers(_globalUntargetedFastHandlers, ref message, emissionId);
2815
- if (_globalUntargetedHandlers?.entries is not { Count: > 0 })
3455
+ HandlerActionCache<FastHandler<IUntargetedMessage>> fastCache = GetGlobalCache<
3456
+ FastHandler<IUntargetedMessage>
3457
+ >(TypedGlobalSlotIndex.UntargetedFast);
3458
+ RunFastHandlers(fastCache, ref message, emissionId);
3459
+ HandlerActionCache<Action<IUntargetedMessage>> cache = GetGlobalCache<
3460
+ Action<IUntargetedMessage>
3461
+ >(TypedGlobalSlotIndex.UntargetedDefault);
3462
+ // Live-count fast path. Cross-handler in-flight snapshot
3463
+ // semantics do not apply to the global accept-all path: the
3464
+ // bus dispatch loop calls PrefreezeGlobalUntargetedForEmission
3465
+ // lazily per-entry inside InvokeGlobalUntargetedEntry, after
3466
+ // earlier-priority handlers have already run. A sibling
3467
+ // MessageHandler that removes this handler's entry mid-emit
3468
+ // drains cache.entries before the lazy prefreeze can capture
3469
+ // a snapshot, so cache.cache rebuilds from the now-empty
3470
+ // entries. Bailing on cache.entries.Count == 0 is therefore
3471
+ // equivalent to bailing after GetOrAddNewHandlerStack would
3472
+ // return an empty list, and is documented behavior for the
3473
+ // global path.
3474
+ if (cache?.entries is not { Count: > 0 })
2816
3475
  {
2817
3476
  return;
2818
3477
  }
2819
3478
 
2820
3479
  List<Action<IUntargetedMessage>> handlers = GetOrAddNewHandlerStack(
2821
- _globalUntargetedHandlers,
3480
+ cache,
2822
3481
  emissionId
2823
3482
  );
2824
3483
  int handlersCount = handlers.Count;
@@ -2839,15 +3498,25 @@ namespace DxMessaging.Core
2839
3498
  long emissionId
2840
3499
  )
2841
3500
  {
2842
- RunFastHandlers(ref target, _globalTargetedFastHandlers, ref message, emissionId);
2843
-
2844
- if (_globalTargetedHandlers?.entries is not { Count: > 0 })
3501
+ HandlerActionCache<FastHandlerWithContext<ITargetedMessage>> fastCache =
3502
+ GetGlobalCache<FastHandlerWithContext<ITargetedMessage>>(
3503
+ TypedGlobalSlotIndex.TargetedFast
3504
+ );
3505
+ RunFastHandlers(ref target, fastCache, ref message, emissionId);
3506
+
3507
+ HandlerActionCache<Action<InstanceId, ITargetedMessage>> cache = GetGlobalCache<
3508
+ Action<InstanceId, ITargetedMessage>
3509
+ >(TypedGlobalSlotIndex.TargetedDefault);
3510
+ // Live-count fast path. See comment in HandleGlobalUntargeted
3511
+ // for why the global accept-all path bails on
3512
+ // cache.entries.Count == 0 rather than reading the snapshot.
3513
+ if (cache?.entries is not { Count: > 0 })
2845
3514
  {
2846
3515
  return;
2847
3516
  }
2848
3517
 
2849
3518
  List<Action<InstanceId, ITargetedMessage>> handlers = GetOrAddNewHandlerStack(
2850
- _globalTargetedHandlers,
3519
+ cache,
2851
3520
  emissionId
2852
3521
  );
2853
3522
  int handlersCount = handlers.Count;
@@ -2868,15 +3537,25 @@ namespace DxMessaging.Core
2868
3537
  long emissionId
2869
3538
  )
2870
3539
  {
2871
- RunFastHandlers(ref source, _globalBroadcastFastHandlers, ref message, emissionId);
2872
-
2873
- if (_globalBroadcastHandlers?.entries is not { Count: > 0 })
3540
+ HandlerActionCache<FastHandlerWithContext<IBroadcastMessage>> fastCache =
3541
+ GetGlobalCache<FastHandlerWithContext<IBroadcastMessage>>(
3542
+ TypedGlobalSlotIndex.BroadcastFast
3543
+ );
3544
+ RunFastHandlers(ref source, fastCache, ref message, emissionId);
3545
+
3546
+ HandlerActionCache<Action<InstanceId, IBroadcastMessage>> cache = GetGlobalCache<
3547
+ Action<InstanceId, IBroadcastMessage>
3548
+ >(TypedGlobalSlotIndex.BroadcastDefault);
3549
+ // Live-count fast path. See comment in HandleGlobalUntargeted
3550
+ // for why the global accept-all path bails on
3551
+ // cache.entries.Count == 0 rather than reading the snapshot.
3552
+ if (cache?.entries is not { Count: > 0 })
2874
3553
  {
2875
3554
  return;
2876
3555
  }
2877
3556
 
2878
3557
  List<Action<InstanceId, IBroadcastMessage>> handlers = GetOrAddNewHandlerStack(
2879
- _globalBroadcastHandlers,
3558
+ cache,
2880
3559
  emissionId
2881
3560
  );
2882
3561
  int handlersCount = handlers.Count;
@@ -2934,12 +3613,17 @@ namespace DxMessaging.Core
2934
3613
  public void HandleUntargetedPostProcessing(ref T message, int priority, long emissionId)
2935
3614
  {
2936
3615
  RunFastHandlers(
2937
- _untargetedPostProcessingFastHandlers,
3616
+ GetPriorityHandlers(TypedSlotIndex.UntargetedPostProcessFast),
3617
+ ref message,
3618
+ priority,
3619
+ emissionId
3620
+ );
3621
+ RunHandlers(
3622
+ GetPriorityHandlers(TypedSlotIndex.UntargetedPostProcessDefault),
2938
3623
  ref message,
2939
3624
  priority,
2940
3625
  emissionId
2941
3626
  );
2942
- RunHandlers(_untargetedPostProcessingHandlers, ref message, priority, emissionId);
2943
3627
  }
2944
3628
 
2945
3629
  /// <summary>
@@ -2958,14 +3642,14 @@ namespace DxMessaging.Core
2958
3642
  {
2959
3643
  RunFastHandlersWithContext(
2960
3644
  ref target,
2961
- _targetedPostProcessingFastHandlers,
3645
+ GetContextHandlers(TypedSlotIndex.TargetedPostProcessFast),
2962
3646
  ref message,
2963
3647
  priority,
2964
3648
  emissionId
2965
3649
  );
2966
3650
  RunHandlersWithContext(
2967
3651
  ref target,
2968
- _targetedPostProcessingHandlers,
3652
+ GetContextHandlers(TypedSlotIndex.TargetedPostProcessDefault),
2969
3653
  ref message,
2970
3654
  priority,
2971
3655
  emissionId
@@ -2988,14 +3672,14 @@ namespace DxMessaging.Core
2988
3672
  {
2989
3673
  RunFastHandlersWithContext(
2990
3674
  ref target,
2991
- _fastTargetedWithoutTargetingPostProcessingHandlers,
3675
+ GetPriorityHandlers(TypedSlotIndex.TargetedPostProcessWithoutContextFast),
2992
3676
  ref message,
2993
3677
  priority,
2994
3678
  emissionId
2995
3679
  );
2996
3680
  RunHandlers(
2997
3681
  ref target,
2998
- _targetedWithoutTargetingPostProcessingHandlers,
3682
+ GetPriorityHandlers(TypedSlotIndex.TargetedPostProcessWithoutContext),
2999
3683
  ref message,
3000
3684
  priority,
3001
3685
  emissionId
@@ -3018,14 +3702,14 @@ namespace DxMessaging.Core
3018
3702
  {
3019
3703
  RunFastHandlersWithContext(
3020
3704
  ref source,
3021
- _broadcastPostProcessingFastHandlers,
3705
+ GetContextHandlers(TypedSlotIndex.BroadcastPostProcessFast),
3022
3706
  ref message,
3023
3707
  priority,
3024
3708
  emissionId
3025
3709
  );
3026
3710
  RunHandlersWithContext(
3027
3711
  ref source,
3028
- _broadcastPostProcessingHandlers,
3712
+ GetContextHandlers(TypedSlotIndex.BroadcastPostProcessDefault),
3029
3713
  ref message,
3030
3714
  priority,
3031
3715
  emissionId
@@ -3048,14 +3732,14 @@ namespace DxMessaging.Core
3048
3732
  {
3049
3733
  RunFastHandlersWithContext(
3050
3734
  ref source,
3051
- _fastBroadcastWithoutSourcePostProcessingHandlers,
3735
+ GetPriorityHandlers(TypedSlotIndex.BroadcastPostProcessWithoutContextFast),
3052
3736
  ref message,
3053
3737
  priority,
3054
3738
  emissionId
3055
3739
  );
3056
3740
  RunHandlers(
3057
3741
  ref source,
3058
- _broadcastWithoutSourcePostProcessingHandlers,
3742
+ GetPriorityHandlers(TypedSlotIndex.BroadcastPostProcessWithoutContext),
3059
3743
  ref message,
3060
3744
  priority,
3061
3745
  emissionId
@@ -3076,17 +3760,17 @@ namespace DxMessaging.Core
3076
3760
  Action<T> handler,
3077
3761
  Action deregistration,
3078
3762
  int priority,
3079
- long emissionId
3763
+ IMessageBus messageBus
3080
3764
  )
3081
3765
  {
3082
- return AddHandler(
3766
+ return AddHandlerPreservingPriorityKey(
3083
3767
  target,
3084
- ref _targetedHandlers,
3768
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedHandleDefault),
3085
3769
  originalHandler,
3086
3770
  handler,
3087
3771
  deregistration,
3088
3772
  priority,
3089
- emissionId
3773
+ messageBus
3090
3774
  );
3091
3775
  }
3092
3776
 
@@ -3104,17 +3788,17 @@ namespace DxMessaging.Core
3104
3788
  FastHandler<T> handler,
3105
3789
  Action deregistration,
3106
3790
  int priority,
3107
- long emissionId
3791
+ IMessageBus messageBus
3108
3792
  )
3109
3793
  {
3110
- return AddHandler(
3794
+ return AddHandlerPreservingPriorityKey(
3111
3795
  target,
3112
- ref _targetedFastHandlers,
3796
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedHandleFast),
3113
3797
  originalHandler,
3114
3798
  handler,
3115
3799
  deregistration,
3116
3800
  priority,
3117
- emissionId
3801
+ messageBus
3118
3802
  );
3119
3803
  }
3120
3804
 
@@ -3130,16 +3814,19 @@ namespace DxMessaging.Core
3130
3814
  Action<InstanceId, T> handler,
3131
3815
  Action deregistration,
3132
3816
  int priority,
3133
- long emissionId
3817
+ IMessageBus messageBus
3134
3818
  )
3135
3819
  {
3136
3820
  return AddHandlerPreservingPriorityKey(
3137
- ref _targetedWithoutTargetingHandlers,
3821
+ GetOrCreatePriorityHandlers(
3822
+ TypedSlotIndex.TargetedHandleWithoutContext,
3823
+ requiresContext: false
3824
+ ),
3138
3825
  originalHandler,
3139
3826
  handler,
3140
3827
  deregistration,
3141
3828
  priority,
3142
- emissionId
3829
+ messageBus
3143
3830
  );
3144
3831
  }
3145
3832
 
@@ -3155,16 +3842,19 @@ namespace DxMessaging.Core
3155
3842
  FastHandlerWithContext<T> handler,
3156
3843
  Action deregistration,
3157
3844
  int priority,
3158
- long emissionId
3845
+ IMessageBus messageBus
3159
3846
  )
3160
3847
  {
3161
3848
  return AddHandlerPreservingPriorityKey(
3162
- ref _fastTargetedWithoutTargetingHandlers,
3849
+ GetOrCreatePriorityHandlers(
3850
+ TypedSlotIndex.TargetedHandleWithoutContextFast,
3851
+ requiresContext: false
3852
+ ),
3163
3853
  originalHandler,
3164
3854
  handler,
3165
3855
  deregistration,
3166
3856
  priority,
3167
- emissionId
3857
+ messageBus
3168
3858
  );
3169
3859
  }
3170
3860
 
@@ -3180,16 +3870,19 @@ namespace DxMessaging.Core
3180
3870
  Action<T> handler,
3181
3871
  Action deregistration,
3182
3872
  int priority,
3183
- long emissionId
3873
+ IMessageBus messageBus
3184
3874
  )
3185
3875
  {
3186
- return AddHandler(
3187
- ref _untargetedHandlers,
3876
+ return AddHandlerPreservingPriorityKey(
3877
+ GetOrCreatePriorityHandlers(
3878
+ TypedSlotIndex.UntargetedHandleDefault,
3879
+ requiresContext: false
3880
+ ),
3188
3881
  originalHandler,
3189
3882
  handler,
3190
3883
  deregistration,
3191
3884
  priority,
3192
- emissionId
3885
+ messageBus
3193
3886
  );
3194
3887
  }
3195
3888
 
@@ -3205,16 +3898,19 @@ namespace DxMessaging.Core
3205
3898
  FastHandler<T> handler,
3206
3899
  Action deregistration,
3207
3900
  int priority,
3208
- long emissionId
3901
+ IMessageBus messageBus
3209
3902
  )
3210
3903
  {
3211
- return AddHandler(
3212
- ref _untargetedFastHandlers,
3904
+ return AddHandlerPreservingPriorityKey(
3905
+ GetOrCreatePriorityHandlers(
3906
+ TypedSlotIndex.UntargetedHandleFast,
3907
+ requiresContext: false
3908
+ ),
3213
3909
  originalHandler,
3214
3910
  handler,
3215
3911
  deregistration,
3216
3912
  priority,
3217
- emissionId
3913
+ messageBus
3218
3914
  );
3219
3915
  }
3220
3916
 
@@ -3232,17 +3928,17 @@ namespace DxMessaging.Core
3232
3928
  Action<T> handler,
3233
3929
  Action deregistration,
3234
3930
  int priority,
3235
- long emissionId
3931
+ IMessageBus messageBus
3236
3932
  )
3237
3933
  {
3238
- return AddHandler(
3934
+ return AddHandlerPreservingPriorityKey(
3239
3935
  source,
3240
- ref _broadcastHandlers,
3936
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastHandleDefault),
3241
3937
  originalHandler,
3242
3938
  handler,
3243
3939
  deregistration,
3244
3940
  priority,
3245
- emissionId
3941
+ messageBus
3246
3942
  );
3247
3943
  }
3248
3944
 
@@ -3260,17 +3956,17 @@ namespace DxMessaging.Core
3260
3956
  FastHandler<T> handler,
3261
3957
  Action deregistration,
3262
3958
  int priority,
3263
- long emissionId
3959
+ IMessageBus messageBus
3264
3960
  )
3265
3961
  {
3266
- return AddHandler(
3962
+ return AddHandlerPreservingPriorityKey(
3267
3963
  source,
3268
- ref _broadcastFastHandlers,
3964
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastHandleFast),
3269
3965
  originalHandler,
3270
3966
  handler,
3271
3967
  deregistration,
3272
3968
  priority,
3273
- emissionId
3969
+ messageBus
3274
3970
  );
3275
3971
  }
3276
3972
 
@@ -3286,17 +3982,20 @@ namespace DxMessaging.Core
3286
3982
  Action<InstanceId, T> handler,
3287
3983
  Action deregistration,
3288
3984
  int priority,
3289
- long emissionId
3985
+ IMessageBus messageBus
3290
3986
  )
3291
3987
  {
3292
3988
  // Preserve the priority bucket during the current emission so frozen snapshots remain valid
3293
3989
  return AddHandlerPreservingPriorityKey(
3294
- ref _broadcastWithoutSourceHandlers,
3990
+ GetOrCreatePriorityHandlers(
3991
+ TypedSlotIndex.BroadcastHandleWithoutContext,
3992
+ requiresContext: false
3993
+ ),
3295
3994
  originalHandler,
3296
3995
  handler,
3297
3996
  deregistration,
3298
3997
  priority,
3299
- emissionId
3998
+ messageBus
3300
3999
  );
3301
4000
  }
3302
4001
 
@@ -3312,17 +4011,20 @@ namespace DxMessaging.Core
3312
4011
  FastHandlerWithContext<T> handler,
3313
4012
  Action deregistration,
3314
4013
  int priority,
3315
- long emissionId
4014
+ IMessageBus messageBus
3316
4015
  )
3317
4016
  {
3318
4017
  // Preserve the priority bucket during the current emission so frozen snapshots remain valid
3319
4018
  return AddHandlerPreservingPriorityKey(
3320
- ref _fastBroadcastWithoutSourceHandlers,
4019
+ GetOrCreatePriorityHandlers(
4020
+ TypedSlotIndex.BroadcastHandleWithoutContextFast,
4021
+ requiresContext: false
4022
+ ),
3321
4023
  originalHandler,
3322
4024
  handler,
3323
4025
  deregistration,
3324
4026
  priority,
3325
- emissionId
4027
+ messageBus
3326
4028
  );
3327
4029
  }
3328
4030
 
@@ -3335,14 +4037,16 @@ namespace DxMessaging.Core
3335
4037
  public Action AddGlobalUntargetedHandler(
3336
4038
  Action<IUntargetedMessage> originalHandler,
3337
4039
  Action<IUntargetedMessage> handler,
3338
- Action deregistration
4040
+ Action deregistration,
4041
+ IMessageBus messageBus
3339
4042
  )
3340
4043
  {
3341
4044
  return AddHandler(
3342
- ref _globalUntargetedHandlers,
4045
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.UntargetedDefault),
3343
4046
  originalHandler,
3344
4047
  handler,
3345
- deregistration
4048
+ deregistration,
4049
+ messageBus
3346
4050
  );
3347
4051
  }
3348
4052
 
@@ -3355,14 +4059,16 @@ namespace DxMessaging.Core
3355
4059
  public Action AddGlobalUntargetedHandler(
3356
4060
  FastHandler<IUntargetedMessage> originalHandler,
3357
4061
  FastHandler<IUntargetedMessage> handler,
3358
- Action deregistration
4062
+ Action deregistration,
4063
+ IMessageBus messageBus
3359
4064
  )
3360
4065
  {
3361
4066
  return AddHandler(
3362
- ref _globalUntargetedFastHandlers,
4067
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.UntargetedFast),
3363
4068
  originalHandler,
3364
4069
  handler,
3365
- deregistration
4070
+ deregistration,
4071
+ messageBus
3366
4072
  );
3367
4073
  }
3368
4074
 
@@ -3375,14 +4081,16 @@ namespace DxMessaging.Core
3375
4081
  public Action AddGlobalTargetedHandler(
3376
4082
  Action<InstanceId, ITargetedMessage> originalHandler,
3377
4083
  Action<InstanceId, ITargetedMessage> handler,
3378
- Action deregistration
4084
+ Action deregistration,
4085
+ IMessageBus messageBus
3379
4086
  )
3380
4087
  {
3381
4088
  return AddHandler(
3382
- ref _globalTargetedHandlers,
4089
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.TargetedDefault),
3383
4090
  originalHandler,
3384
4091
  handler,
3385
- deregistration
4092
+ deregistration,
4093
+ messageBus
3386
4094
  );
3387
4095
  }
3388
4096
 
@@ -3395,14 +4103,16 @@ namespace DxMessaging.Core
3395
4103
  public Action AddGlobalTargetedHandler(
3396
4104
  FastHandlerWithContext<ITargetedMessage> originalHandler,
3397
4105
  FastHandlerWithContext<ITargetedMessage> handler,
3398
- Action deregistration
4106
+ Action deregistration,
4107
+ IMessageBus messageBus
3399
4108
  )
3400
4109
  {
3401
4110
  return AddHandler(
3402
- ref _globalTargetedFastHandlers,
4111
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.TargetedFast),
3403
4112
  originalHandler,
3404
4113
  handler,
3405
- deregistration
4114
+ deregistration,
4115
+ messageBus
3406
4116
  );
3407
4117
  }
3408
4118
 
@@ -3415,14 +4125,16 @@ namespace DxMessaging.Core
3415
4125
  public Action AddGlobalBroadcastHandler(
3416
4126
  Action<InstanceId, IBroadcastMessage> originalHandler,
3417
4127
  Action<InstanceId, IBroadcastMessage> handler,
3418
- Action deregistration
4128
+ Action deregistration,
4129
+ IMessageBus messageBus
3419
4130
  )
3420
4131
  {
3421
4132
  return AddHandler(
3422
- ref _globalBroadcastHandlers,
4133
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.BroadcastDefault),
3423
4134
  originalHandler,
3424
4135
  handler,
3425
- deregistration
4136
+ deregistration,
4137
+ messageBus
3426
4138
  );
3427
4139
  }
3428
4140
 
@@ -3435,14 +4147,16 @@ namespace DxMessaging.Core
3435
4147
  public Action AddGlobalBroadcastHandler(
3436
4148
  FastHandlerWithContext<IBroadcastMessage> originalHandler,
3437
4149
  FastHandlerWithContext<IBroadcastMessage> handler,
3438
- Action deregistration
4150
+ Action deregistration,
4151
+ IMessageBus messageBus
3439
4152
  )
3440
4153
  {
3441
4154
  return AddHandler(
3442
- ref _globalBroadcastFastHandlers,
4155
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.BroadcastFast),
3443
4156
  originalHandler,
3444
4157
  handler,
3445
- deregistration
4158
+ deregistration,
4159
+ messageBus
3446
4160
  );
3447
4161
  }
3448
4162
 
@@ -3458,16 +4172,19 @@ namespace DxMessaging.Core
3458
4172
  Action<T> handler,
3459
4173
  Action deregistration,
3460
4174
  int priority,
3461
- long emissionId
4175
+ IMessageBus messageBus
3462
4176
  )
3463
4177
  {
3464
4178
  return AddHandlerPreservingPriorityKey(
3465
- ref _untargetedPostProcessingHandlers,
4179
+ GetOrCreatePriorityHandlers(
4180
+ TypedSlotIndex.UntargetedPostProcessDefault,
4181
+ requiresContext: false
4182
+ ),
3466
4183
  originalHandler,
3467
4184
  handler,
3468
4185
  deregistration,
3469
4186
  priority,
3470
- emissionId
4187
+ messageBus
3471
4188
  );
3472
4189
  }
3473
4190
 
@@ -3483,16 +4200,19 @@ namespace DxMessaging.Core
3483
4200
  FastHandler<T> handler,
3484
4201
  Action deregistration,
3485
4202
  int priority,
3486
- long emissionId
4203
+ IMessageBus messageBus
3487
4204
  )
3488
4205
  {
3489
4206
  return AddHandlerPreservingPriorityKey(
3490
- ref _untargetedPostProcessingFastHandlers,
4207
+ GetOrCreatePriorityHandlers(
4208
+ TypedSlotIndex.UntargetedPostProcessFast,
4209
+ requiresContext: false
4210
+ ),
3491
4211
  originalHandler,
3492
4212
  handler,
3493
4213
  deregistration,
3494
4214
  priority,
3495
- emissionId
4215
+ messageBus
3496
4216
  );
3497
4217
  }
3498
4218
 
@@ -3510,17 +4230,17 @@ namespace DxMessaging.Core
3510
4230
  Action<T> handler,
3511
4231
  Action deregistration,
3512
4232
  int priority,
3513
- long emissionId
4233
+ IMessageBus messageBus
3514
4234
  )
3515
4235
  {
3516
4236
  return AddHandlerPreservingPriorityKey(
3517
4237
  target,
3518
- ref _targetedPostProcessingHandlers,
4238
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedPostProcessDefault),
3519
4239
  originalHandler,
3520
4240
  handler,
3521
4241
  deregistration,
3522
4242
  priority,
3523
- emissionId
4243
+ messageBus
3524
4244
  );
3525
4245
  }
3526
4246
 
@@ -3538,17 +4258,17 @@ namespace DxMessaging.Core
3538
4258
  FastHandler<T> handler,
3539
4259
  Action deregistration,
3540
4260
  int priority,
3541
- long emissionId
4261
+ IMessageBus messageBus
3542
4262
  )
3543
4263
  {
3544
4264
  return AddHandlerPreservingPriorityKey(
3545
4265
  target,
3546
- ref _targetedPostProcessingFastHandlers,
4266
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedPostProcessFast),
3547
4267
  originalHandler,
3548
4268
  handler,
3549
4269
  deregistration,
3550
4270
  priority,
3551
- emissionId
4271
+ messageBus
3552
4272
  );
3553
4273
  }
3554
4274
 
@@ -3564,16 +4284,19 @@ namespace DxMessaging.Core
3564
4284
  Action<InstanceId, T> handler,
3565
4285
  Action deregistration,
3566
4286
  int priority,
3567
- long emissionId
4287
+ IMessageBus messageBus
3568
4288
  )
3569
4289
  {
3570
4290
  return AddHandlerPreservingPriorityKey(
3571
- ref _targetedWithoutTargetingPostProcessingHandlers,
4291
+ GetOrCreatePriorityHandlers(
4292
+ TypedSlotIndex.TargetedPostProcessWithoutContext,
4293
+ requiresContext: false
4294
+ ),
3572
4295
  originalHandler,
3573
4296
  handler,
3574
4297
  deregistration,
3575
4298
  priority,
3576
- emissionId
4299
+ messageBus
3577
4300
  );
3578
4301
  }
3579
4302
 
@@ -3589,16 +4312,19 @@ namespace DxMessaging.Core
3589
4312
  FastHandlerWithContext<T> handler,
3590
4313
  Action deregistration,
3591
4314
  int priority,
3592
- long emissionId
4315
+ IMessageBus messageBus
3593
4316
  )
3594
4317
  {
3595
4318
  return AddHandlerPreservingPriorityKey(
3596
- ref _fastTargetedWithoutTargetingPostProcessingHandlers,
4319
+ GetOrCreatePriorityHandlers(
4320
+ TypedSlotIndex.TargetedPostProcessWithoutContextFast,
4321
+ requiresContext: false
4322
+ ),
3597
4323
  originalHandler,
3598
4324
  handler,
3599
4325
  deregistration,
3600
4326
  priority,
3601
- emissionId
4327
+ messageBus
3602
4328
  );
3603
4329
  }
3604
4330
 
@@ -3616,17 +4342,17 @@ namespace DxMessaging.Core
3616
4342
  Action<T> handler,
3617
4343
  Action deregistration,
3618
4344
  int priority,
3619
- long emissionId
4345
+ IMessageBus messageBus
3620
4346
  )
3621
4347
  {
3622
4348
  return AddHandlerPreservingPriorityKey(
3623
4349
  source,
3624
- ref _broadcastPostProcessingHandlers,
4350
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastPostProcessDefault),
3625
4351
  originalHandler,
3626
4352
  handler,
3627
4353
  deregistration,
3628
4354
  priority,
3629
- emissionId
4355
+ messageBus
3630
4356
  );
3631
4357
  }
3632
4358
 
@@ -3644,17 +4370,17 @@ namespace DxMessaging.Core
3644
4370
  FastHandler<T> handler,
3645
4371
  Action deregistration,
3646
4372
  int priority,
3647
- long emissionId
4373
+ IMessageBus messageBus
3648
4374
  )
3649
4375
  {
3650
4376
  return AddHandlerPreservingPriorityKey(
3651
4377
  source,
3652
- ref _broadcastPostProcessingFastHandlers,
4378
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastPostProcessFast),
3653
4379
  originalHandler,
3654
4380
  handler,
3655
4381
  deregistration,
3656
4382
  priority,
3657
- emissionId
4383
+ messageBus
3658
4384
  );
3659
4385
  }
3660
4386
 
@@ -3670,16 +4396,19 @@ namespace DxMessaging.Core
3670
4396
  Action<InstanceId, T> handler,
3671
4397
  Action deregistration,
3672
4398
  int priority,
3673
- long emissionId
4399
+ IMessageBus messageBus
3674
4400
  )
3675
4401
  {
3676
4402
  return AddHandlerPreservingPriorityKey(
3677
- ref _broadcastWithoutSourcePostProcessingHandlers,
4403
+ GetOrCreatePriorityHandlers(
4404
+ TypedSlotIndex.BroadcastPostProcessWithoutContext,
4405
+ requiresContext: false
4406
+ ),
3678
4407
  originalHandler,
3679
4408
  handler,
3680
4409
  deregistration,
3681
4410
  priority,
3682
- emissionId
4411
+ messageBus
3683
4412
  );
3684
4413
  }
3685
4414
 
@@ -3695,36 +4424,197 @@ namespace DxMessaging.Core
3695
4424
  FastHandlerWithContext<T> handler,
3696
4425
  Action deregistration,
3697
4426
  int priority,
3698
- long emissionId
4427
+ IMessageBus messageBus
3699
4428
  )
3700
4429
  {
3701
4430
  return AddHandlerPreservingPriorityKey(
3702
- ref _fastBroadcastWithoutSourcePostProcessingHandlers,
4431
+ GetOrCreatePriorityHandlers(
4432
+ TypedSlotIndex.BroadcastPostProcessWithoutContextFast,
4433
+ requiresContext: false
4434
+ ),
3703
4435
  originalHandler,
3704
4436
  handler,
3705
4437
  deregistration,
3706
4438
  priority,
3707
- emissionId
4439
+ messageBus
3708
4440
  );
3709
4441
  }
3710
4442
 
3711
- // Context-aware variant that preserves the priority key mapping on deregistration for the current emission.
3712
- private static Action AddHandlerPreservingPriorityKey<TU>(
4443
+ // Context-aware variant that preserves the priority and context key
4444
+ // mappings on deregistration so frozen dispatch snapshots remain valid
4445
+ // for any in-flight emission. Trade-off: empty HandlerActionCache
4446
+ // entries (and their enclosing per-priority Dictionary) are not
4447
+ // reclaimed until either (a) a future registration at the same
4448
+ // (context, priority) pair reuses the cache, or (b) the owning
4449
+ // MessageHandler is destroyed. For typical Unity gameplay (a small
4450
+ // fixed set of priorities and a bounded set of long-lived target /
4451
+ // source InstanceIds) the residual footprint is on the order of
4452
+ // hundreds of bytes per MessageHandler. Code that interacts with
4453
+ // many transient InstanceIds (e.g. a global service that registers
4454
+ // handlers per ephemeral GameObject) should prefer recycling
4455
+ // MessageHandlers or routing through AddSourcedBroadcastWithoutSourceHandler /
4456
+ // AddTargetedWithoutTargetingHandler to avoid the per-(context,priority)
4457
+ // outer-dictionary growth.
4458
+ private Action AddHandlerPreservingPriorityKey<TU>(
3713
4459
  InstanceId context,
3714
- ref Dictionary<
3715
- InstanceId,
3716
- Dictionary<int, HandlerActionCache<TU>>
3717
- > handlersByContext,
4460
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext,
3718
4461
  TU originalHandler,
3719
4462
  TU augmentedHandler,
3720
4463
  Action deregistration,
3721
4464
  int priority,
3722
- long emissionId
4465
+ IMessageBus messageBus
3723
4466
  )
3724
4467
  {
3725
- handlersByContext ??=
3726
- new Dictionary<InstanceId, Dictionary<int, HandlerActionCache<TU>>>();
3727
-
4468
+ if (
4469
+ !handlersByContext.TryGetValue(
4470
+ context,
4471
+ out Dictionary<int, IHandlerActionCache> sortedHandlers
4472
+ )
4473
+ )
4474
+ {
4475
+ sortedHandlers = DxPools.TypedHandlerPriorityDicts.Rent();
4476
+ handlersByContext[context] = sortedHandlers;
4477
+ }
4478
+
4479
+ if (
4480
+ !sortedHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
4481
+ || erasedCache is not HandlerActionCache<TU> cache
4482
+ )
4483
+ {
4484
+ cache = new HandlerActionCache<TU>();
4485
+ sortedHandlers[priority] = cache;
4486
+ }
4487
+
4488
+ if (
4489
+ !cache.entries.TryGetValue(
4490
+ originalHandler,
4491
+ out HandlerActionCache<TU>.Entry entry
4492
+ )
4493
+ )
4494
+ {
4495
+ entry = new HandlerActionCache<TU>.Entry(augmentedHandler, 0);
4496
+ }
4497
+
4498
+ bool firstRegistration = entry.count == 0;
4499
+ entry = firstRegistration
4500
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1)
4501
+ : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
4502
+
4503
+ cache.entries[originalHandler] = entry;
4504
+ cache.version++;
4505
+ TypedSlot<T> slot = FindContextSlot(handlersByContext);
4506
+ if (slot != null)
4507
+ {
4508
+ slot.lastTouchTicks =
4509
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
4510
+ messageBus
4511
+ );
4512
+ }
4513
+ if (firstRegistration && slot != null)
4514
+ {
4515
+ slot.liveCount++;
4516
+ }
4517
+
4518
+ Dictionary<
4519
+ InstanceId,
4520
+ Dictionary<int, IHandlerActionCache>
4521
+ > localHandlersByContext = handlersByContext;
4522
+ TypedSlot<T> localSlot = slot;
4523
+ long localSlotVersion = slot?.version ?? 0;
4524
+ long localResetGeneration =
4525
+ global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
4526
+
4527
+ return () =>
4528
+ {
4529
+ if (
4530
+ !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
4531
+ messageBus,
4532
+ localResetGeneration
4533
+ )
4534
+ )
4535
+ {
4536
+ return;
4537
+ }
4538
+
4539
+ if (localSlot != null && localSlot.version != localSlotVersion)
4540
+ {
4541
+ return;
4542
+ }
4543
+
4544
+ if (!localHandlersByContext.TryGetValue(context, out sortedHandlers))
4545
+ {
4546
+ return;
4547
+ }
4548
+
4549
+ if (
4550
+ !sortedHandlers.TryGetValue(
4551
+ priority,
4552
+ out IHandlerActionCache localErasedCache
4553
+ ) || localErasedCache is not HandlerActionCache<TU> localCache
4554
+ )
4555
+ {
4556
+ return;
4557
+ }
4558
+
4559
+ if (
4560
+ !localCache.entries.TryGetValue(
4561
+ originalHandler,
4562
+ out HandlerActionCache<TU>.Entry localEntry
4563
+ )
4564
+ )
4565
+ {
4566
+ return;
4567
+ }
4568
+
4569
+ localCache.version++;
4570
+
4571
+ deregistration?.Invoke();
4572
+ if (localSlot != null)
4573
+ {
4574
+ localSlot.lastTouchTicks =
4575
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
4576
+ messageBus
4577
+ );
4578
+ }
4579
+
4580
+ if (localEntry.count <= 1)
4581
+ {
4582
+ _ = localCache.entries.Remove(originalHandler);
4583
+ localCache.version++;
4584
+ if (localSlot != null)
4585
+ {
4586
+ localSlot.liveCount--;
4587
+ }
4588
+ // Deliberately keep the priority and context mappings to preserve
4589
+ // frozen snapshots for the current emission.
4590
+ return;
4591
+ }
4592
+
4593
+ localEntry = new HandlerActionCache<TU>.Entry(
4594
+ localEntry.handler,
4595
+ localEntry.count - 1
4596
+ );
4597
+
4598
+ localCache.entries[originalHandler] = localEntry;
4599
+ };
4600
+ }
4601
+
4602
+ private static Action AddHandlerPreservingPriorityKey<TU>(
4603
+ InstanceId context,
4604
+ ref Dictionary<
4605
+ InstanceId,
4606
+ Dictionary<int, HandlerActionCache<TU>>
4607
+ > handlersByContext,
4608
+ TU originalHandler,
4609
+ TU augmentedHandler,
4610
+ Action deregistration,
4611
+ int priority,
4612
+ IMessageBus messageBus
4613
+ )
4614
+ {
4615
+ handlersByContext ??=
4616
+ new Dictionary<InstanceId, Dictionary<int, HandlerActionCache<TU>>>();
4617
+
3728
4618
  if (
3729
4619
  !handlersByContext.TryGetValue(
3730
4620
  context,
@@ -3811,6 +4701,41 @@ namespace DxMessaging.Core
3811
4701
  };
3812
4702
  }
3813
4703
 
4704
+ private static void RunFastHandlersWithContext<TMessage>(
4705
+ ref InstanceId context,
4706
+ Dictionary<int, IHandlerActionCache> fastHandlers,
4707
+ ref TMessage message,
4708
+ int priority,
4709
+ long emissionId
4710
+ )
4711
+ where TMessage : IMessage
4712
+ {
4713
+ RunFastHandlers(ref context, fastHandlers, ref message, priority, emissionId);
4714
+ }
4715
+
4716
+ private static void RunFastHandlersWithContext<TMessage>(
4717
+ ref InstanceId context,
4718
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> fastHandlersByContext,
4719
+ ref TMessage message,
4720
+ int priority,
4721
+ long emissionId
4722
+ )
4723
+ where TMessage : IMessage
4724
+ {
4725
+ if (
4726
+ fastHandlersByContext is not { Count: > 0 }
4727
+ || !fastHandlersByContext.TryGetValue(
4728
+ context,
4729
+ out Dictionary<int, IHandlerActionCache> cache
4730
+ )
4731
+ )
4732
+ {
4733
+ return;
4734
+ }
4735
+
4736
+ RunFastHandlers(cache, ref message, priority, emissionId);
4737
+ }
4738
+
3814
4739
  private static void RunFastHandlersWithContext<TMessage>(
3815
4740
  ref InstanceId context,
3816
4741
  Dictionary<
@@ -3863,6 +4788,75 @@ namespace DxMessaging.Core
3863
4788
  RunFastHandlers(cache, ref message, priority, emissionId);
3864
4789
  }
3865
4790
 
4791
+ private static void RunFastHandlers<TMessage>(
4792
+ Dictionary<int, IHandlerActionCache> fastHandlers,
4793
+ ref TMessage message,
4794
+ int priority,
4795
+ long emissionId
4796
+ )
4797
+ where TMessage : IMessage
4798
+ {
4799
+ if (fastHandlers is not { Count: > 0 })
4800
+ {
4801
+ return;
4802
+ }
4803
+
4804
+ if (
4805
+ !fastHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
4806
+ || erasedCache is not HandlerActionCache<FastHandler<T>> cache
4807
+ )
4808
+ {
4809
+ return;
4810
+ }
4811
+
4812
+ ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
4813
+ List<FastHandler<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
4814
+ int handlersCount = handlers.Count;
4815
+ switch (handlersCount)
4816
+ {
4817
+ case 1:
4818
+ {
4819
+ handlers[0](ref typedMessage);
4820
+ return;
4821
+ }
4822
+ case 2:
4823
+ {
4824
+ handlers[0](ref typedMessage);
4825
+ handlers[1](ref typedMessage);
4826
+ return;
4827
+ }
4828
+ case 3:
4829
+ {
4830
+ handlers[0](ref typedMessage);
4831
+ handlers[1](ref typedMessage);
4832
+ handlers[2](ref typedMessage);
4833
+ return;
4834
+ }
4835
+ case 4:
4836
+ {
4837
+ handlers[0](ref typedMessage);
4838
+ handlers[1](ref typedMessage);
4839
+ handlers[2](ref typedMessage);
4840
+ handlers[3](ref typedMessage);
4841
+ return;
4842
+ }
4843
+ case 5:
4844
+ {
4845
+ handlers[0](ref typedMessage);
4846
+ handlers[1](ref typedMessage);
4847
+ handlers[2](ref typedMessage);
4848
+ handlers[3](ref typedMessage);
4849
+ handlers[4](ref typedMessage);
4850
+ return;
4851
+ }
4852
+ }
4853
+
4854
+ for (int i = 0; i < handlersCount; ++i)
4855
+ {
4856
+ handlers[i](ref typedMessage);
4857
+ }
4858
+ }
4859
+
3866
4860
  private static void RunFastHandlers<TMessage>(
3867
4861
  Dictionary<int, HandlerActionCache<FastHandler<T>>> fastHandlers,
3868
4862
  ref TMessage message,
@@ -3942,7 +4936,22 @@ namespace DxMessaging.Core
3942
4936
  where TMessage : IMessage
3943
4937
  where TU : IMessage
3944
4938
  {
3945
- if (cache?.entries is not { Count: > 0 })
4939
+ // Snapshot semantics: do not bail on the live entries dictionary
4940
+ // count. A mid-emit removal can drain entries while the pinned
4941
+ // emission snapshot in cache.cache still holds the handlers we
4942
+ // must invoke. Read the snapshot first and bail only if the
4943
+ // snapshot itself is empty.
4944
+ //
4945
+ // Perf note: GetOrAddNewHandlerStack is now invoked on every
4946
+ // call (including for empty caches that the previous fast-path
4947
+ // would have skipped). The cost is one dictionary
4948
+ // emission-id/version compare and -- only when the per-emission
4949
+ // snapshot has not been pinned yet -- a single pass over
4950
+ // cache.entries to materialise an empty list. The win is
4951
+ // correctness across cross-handler mid-emit removals where the
4952
+ // pinned snapshot in cache.cache still holds handlers the live
4953
+ // entries dictionary no longer reaches.
4954
+ if (cache == null)
3946
4955
  {
3947
4956
  return;
3948
4957
  }
@@ -3950,6 +4959,10 @@ namespace DxMessaging.Core
3950
4959
  ref TU typedMessage = ref Unsafe.As<TMessage, TU>(ref message);
3951
4960
  List<FastHandler<TU>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
3952
4961
  int handlersCount = handlers.Count;
4962
+ if (handlersCount == 0)
4963
+ {
4964
+ return;
4965
+ }
3953
4966
  switch (handlersCount)
3954
4967
  {
3955
4968
  case 1:
@@ -4004,7 +5017,10 @@ namespace DxMessaging.Core
4004
5017
  where TMessage : IMessage
4005
5018
  where TU : IMessage
4006
5019
  {
4007
- if (cache?.entries is not { Count: > 0 })
5020
+ // Snapshot semantics: see comment on the FastHandler<TU> overload.
5021
+ // The pinned emission snapshot may still hold handlers even when
5022
+ // the live entries dictionary has been drained mid-emit.
5023
+ if (cache == null)
4008
5024
  {
4009
5025
  return;
4010
5026
  }
@@ -4015,6 +5031,10 @@ namespace DxMessaging.Core
4015
5031
  emissionId
4016
5032
  );
4017
5033
  int handlersCount = handlers.Count;
5034
+ if (handlersCount == 0)
5035
+ {
5036
+ return;
5037
+ }
4018
5038
  switch (handlersCount)
4019
5039
  {
4020
5040
  case 1:
@@ -4060,6 +5080,57 @@ namespace DxMessaging.Core
4060
5080
  }
4061
5081
  }
4062
5082
 
5083
+ private static void RunFastHandlers<TMessage>(
5084
+ ref InstanceId context,
5085
+ Dictionary<int, IHandlerActionCache> fastHandlers,
5086
+ ref TMessage message,
5087
+ int priority,
5088
+ long emissionId
5089
+ )
5090
+ where TMessage : IMessage
5091
+ {
5092
+ if (fastHandlers is not { Count: > 0 })
5093
+ {
5094
+ return;
5095
+ }
5096
+
5097
+ if (
5098
+ !fastHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5099
+ || erasedCache is not HandlerActionCache<FastHandlerWithContext<T>> cache
5100
+ )
5101
+ {
5102
+ return;
5103
+ }
5104
+
5105
+ RunFastHandlers(ref context, cache, ref message, emissionId);
5106
+ }
5107
+
5108
+ private static void RunFastHandlers<TMessage, TU>(
5109
+ ref InstanceId context,
5110
+ Dictionary<int, IHandlerActionCache> fastHandlers,
5111
+ ref TMessage message,
5112
+ int priority,
5113
+ long emissionId
5114
+ )
5115
+ where TMessage : IMessage
5116
+ where TU : IMessage
5117
+ {
5118
+ if (fastHandlers is not { Count: > 0 })
5119
+ {
5120
+ return;
5121
+ }
5122
+
5123
+ if (
5124
+ !fastHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5125
+ || erasedCache is not HandlerActionCache<FastHandlerWithContext<TU>> cache
5126
+ )
5127
+ {
5128
+ return;
5129
+ }
5130
+
5131
+ RunFastHandlers(ref context, cache, ref message, emissionId);
5132
+ }
5133
+
4063
5134
  private static void RunFastHandlers<TMessage, TU>(
4064
5135
  ref InstanceId context,
4065
5136
  Dictionary<int, HandlerActionCache<FastHandlerWithContext<TU>>> fastHandlers,
@@ -4136,6 +5207,29 @@ namespace DxMessaging.Core
4136
5207
  }
4137
5208
  }
4138
5209
 
5210
+ private static void RunHandlersWithContext<TMessage>(
5211
+ ref InstanceId context,
5212
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext,
5213
+ ref TMessage message,
5214
+ int priority,
5215
+ long emissionId
5216
+ )
5217
+ where TMessage : IMessage
5218
+ {
5219
+ if (
5220
+ handlersByContext is not { Count: > 0 }
5221
+ || !handlersByContext.TryGetValue(
5222
+ context,
5223
+ out Dictionary<int, IHandlerActionCache> cache
5224
+ )
5225
+ )
5226
+ {
5227
+ return;
5228
+ }
5229
+
5230
+ RunHandlers(cache, ref message, priority, emissionId);
5231
+ }
5232
+
4139
5233
  private static void RunHandlersWithContext<TMessage>(
4140
5234
  ref InstanceId context,
4141
5235
  Dictionary<
@@ -4156,10 +5250,79 @@ namespace DxMessaging.Core
4156
5250
  )
4157
5251
  )
4158
5252
  {
4159
- return;
5253
+ return;
5254
+ }
5255
+
5256
+ RunHandlers(cache, ref message, priority, emissionId);
5257
+ }
5258
+
5259
+ private static void RunHandlers<TMessage>(
5260
+ Dictionary<int, IHandlerActionCache> sortedHandlers,
5261
+ ref TMessage message,
5262
+ int priority,
5263
+ long emissionId
5264
+ )
5265
+ where TMessage : IMessage
5266
+ {
5267
+ if (sortedHandlers is not { Count: > 0 })
5268
+ {
5269
+ return;
5270
+ }
5271
+
5272
+ if (
5273
+ !sortedHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5274
+ || erasedCache is not HandlerActionCache<Action<T>> cache
5275
+ )
5276
+ {
5277
+ return;
5278
+ }
5279
+
5280
+ List<Action<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
5281
+ ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
5282
+ int handlersCount = handlers.Count;
5283
+ switch (handlersCount)
5284
+ {
5285
+ case 1:
5286
+ {
5287
+ handlers[0](typedMessage);
5288
+ return;
5289
+ }
5290
+ case 2:
5291
+ {
5292
+ handlers[0](typedMessage);
5293
+ handlers[1](typedMessage);
5294
+ return;
5295
+ }
5296
+ case 3:
5297
+ {
5298
+ handlers[0](typedMessage);
5299
+ handlers[1](typedMessage);
5300
+ handlers[2](typedMessage);
5301
+ return;
5302
+ }
5303
+ case 4:
5304
+ {
5305
+ handlers[0](typedMessage);
5306
+ handlers[1](typedMessage);
5307
+ handlers[2](typedMessage);
5308
+ handlers[3](typedMessage);
5309
+ return;
5310
+ }
5311
+ case 5:
5312
+ {
5313
+ handlers[0](typedMessage);
5314
+ handlers[1](typedMessage);
5315
+ handlers[2](typedMessage);
5316
+ handlers[3](typedMessage);
5317
+ handlers[4](typedMessage);
5318
+ return;
5319
+ }
5320
+ }
5321
+
5322
+ for (int i = 0; i < handlersCount; ++i)
5323
+ {
5324
+ handlers[i](typedMessage);
4160
5325
  }
4161
-
4162
- RunHandlers(cache, ref message, priority, emissionId);
4163
5326
  }
4164
5327
 
4165
5328
  private static void RunHandlers<TMessage>(
@@ -4228,6 +5391,79 @@ namespace DxMessaging.Core
4228
5391
  }
4229
5392
  }
4230
5393
 
5394
+ private static void RunHandlers<TMessage>(
5395
+ ref InstanceId context,
5396
+ Dictionary<int, IHandlerActionCache> handlers,
5397
+ ref TMessage message,
5398
+ int priority,
5399
+ long emissionId
5400
+ )
5401
+ where TMessage : IMessage
5402
+ {
5403
+ if (handlers is not { Count: > 0 })
5404
+ {
5405
+ return;
5406
+ }
5407
+
5408
+ if (
5409
+ !handlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5410
+ || erasedCache is not HandlerActionCache<Action<InstanceId, T>> cache
5411
+ )
5412
+ {
5413
+ return;
5414
+ }
5415
+
5416
+ List<Action<InstanceId, T>> typedHandlers = GetOrAddNewHandlerStack(
5417
+ cache,
5418
+ emissionId
5419
+ );
5420
+ ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
5421
+ int handlersCount = typedHandlers.Count;
5422
+ switch (handlersCount)
5423
+ {
5424
+ case 1:
5425
+ {
5426
+ typedHandlers[0](context, typedMessage);
5427
+ return;
5428
+ }
5429
+ case 2:
5430
+ {
5431
+ typedHandlers[0](context, typedMessage);
5432
+ typedHandlers[1](context, typedMessage);
5433
+ return;
5434
+ }
5435
+ case 3:
5436
+ {
5437
+ typedHandlers[0](context, typedMessage);
5438
+ typedHandlers[1](context, typedMessage);
5439
+ typedHandlers[2](context, typedMessage);
5440
+ return;
5441
+ }
5442
+ case 4:
5443
+ {
5444
+ typedHandlers[0](context, typedMessage);
5445
+ typedHandlers[1](context, typedMessage);
5446
+ typedHandlers[2](context, typedMessage);
5447
+ typedHandlers[3](context, typedMessage);
5448
+ return;
5449
+ }
5450
+ case 5:
5451
+ {
5452
+ typedHandlers[0](context, typedMessage);
5453
+ typedHandlers[1](context, typedMessage);
5454
+ typedHandlers[2](context, typedMessage);
5455
+ typedHandlers[3](context, typedMessage);
5456
+ typedHandlers[4](context, typedMessage);
5457
+ return;
5458
+ }
5459
+ }
5460
+
5461
+ for (int i = 0; i < handlersCount; ++i)
5462
+ {
5463
+ typedHandlers[i](context, typedMessage);
5464
+ }
5465
+ }
5466
+
4231
5467
  private static void RunHandlers<TMessage>(
4232
5468
  ref InstanceId context,
4233
5469
  Dictionary<int, HandlerActionCache<Action<InstanceId, T>>> handlers,
@@ -4330,6 +5566,23 @@ namespace DxMessaging.Core
4330
5566
  return actionCache.cache;
4331
5567
  }
4332
5568
 
5569
+ private static void PrefreezeHandlersForEmission<THandler>(
5570
+ Dictionary<int, IHandlerActionCache> handlers,
5571
+ int priority,
5572
+ long emissionId
5573
+ )
5574
+ {
5575
+ if (
5576
+ handlers != null
5577
+ && handlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5578
+ && erasedCache is HandlerActionCache<THandler> cache
5579
+ )
5580
+ {
5581
+ cache.prefreezeInvocationCount++;
5582
+ _ = GetOrAddNewHandlerStack(cache, emissionId);
5583
+ }
5584
+ }
5585
+
4333
5586
  private static void PrefreezeHandlersForEmission<THandler>(
4334
5587
  Dictionary<int, HandlerActionCache<THandler>> handlers,
4335
5588
  int priority,
@@ -4346,6 +5599,102 @@ namespace DxMessaging.Core
4346
5599
  }
4347
5600
  }
4348
5601
 
5602
+ private static Action AddHandler<TU>(
5603
+ TypedGlobalSlot slot,
5604
+ TU originalHandler,
5605
+ TU augmentedHandler,
5606
+ Action deregistration,
5607
+ IMessageBus messageBus
5608
+ )
5609
+ {
5610
+ slot.lastTouchTicks =
5611
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(messageBus);
5612
+ HandlerActionCache<TU> cache = slot.cache as HandlerActionCache<TU>;
5613
+ if (cache == null)
5614
+ {
5615
+ cache = new HandlerActionCache<TU>();
5616
+ slot.cache = cache;
5617
+ }
5618
+
5619
+ if (
5620
+ !cache.entries.TryGetValue(
5621
+ originalHandler,
5622
+ out HandlerActionCache<TU>.Entry entry
5623
+ )
5624
+ )
5625
+ {
5626
+ entry = new HandlerActionCache<TU>.Entry(augmentedHandler, 0);
5627
+ }
5628
+
5629
+ bool firstRegistration = entry.count == 0;
5630
+ entry = firstRegistration
5631
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1)
5632
+ : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5633
+
5634
+ cache.entries[originalHandler] = entry;
5635
+ cache.version++;
5636
+ if (firstRegistration)
5637
+ {
5638
+ slot.liveCount++;
5639
+ }
5640
+
5641
+ HandlerActionCache<TU> localCache = cache;
5642
+ TypedGlobalSlot localSlot = slot;
5643
+ long localSlotVersion = slot.version;
5644
+ long localResetGeneration =
5645
+ global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
5646
+
5647
+ return () =>
5648
+ {
5649
+ if (
5650
+ !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
5651
+ messageBus,
5652
+ localResetGeneration
5653
+ )
5654
+ )
5655
+ {
5656
+ return;
5657
+ }
5658
+
5659
+ if (localSlot.version != localSlotVersion)
5660
+ {
5661
+ return;
5662
+ }
5663
+
5664
+ if (
5665
+ !localCache.entries.TryGetValue(
5666
+ originalHandler,
5667
+ out HandlerActionCache<TU>.Entry localEntry
5668
+ )
5669
+ )
5670
+ {
5671
+ return;
5672
+ }
5673
+
5674
+ localCache.version++;
5675
+
5676
+ deregistration?.Invoke();
5677
+ localSlot.lastTouchTicks =
5678
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
5679
+ messageBus
5680
+ );
5681
+
5682
+ if (localEntry.count <= 1)
5683
+ {
5684
+ _ = localCache.entries.Remove(originalHandler);
5685
+ localCache.version++;
5686
+ localSlot.liveCount--;
5687
+ return;
5688
+ }
5689
+
5690
+ localEntry = new HandlerActionCache<TU>.Entry(
5691
+ localEntry.handler,
5692
+ localEntry.count - 1
5693
+ );
5694
+ localCache.entries[originalHandler] = localEntry;
5695
+ };
5696
+ }
5697
+
4349
5698
  private static Action AddHandler<TU>(
4350
5699
  InstanceId context,
4351
5700
  ref Dictionary<
@@ -4356,7 +5705,7 @@ namespace DxMessaging.Core
4356
5705
  TU augmentedHandler,
4357
5706
  Action deregistration,
4358
5707
  int priority,
4359
- long emissionId
5708
+ IMessageBus messageBus
4360
5709
  )
4361
5710
  {
4362
5711
  handlersByContext ??=
@@ -4597,6 +5946,134 @@ namespace DxMessaging.Core
4597
5946
  // Variant of AddHandler that preserves the priority key in the dictionary when the last entry is removed.
4598
5947
  // This ensures that during an in-flight emission (where handler stacks are already frozen),
4599
5948
  // subsequent removals do not cause lookups to fail for the current pass.
5949
+ private Action AddHandlerPreservingPriorityKey<TU>(
5950
+ Dictionary<int, IHandlerActionCache> handlers,
5951
+ TU originalHandler,
5952
+ TU augmentedHandler,
5953
+ Action deregistration,
5954
+ int priority,
5955
+ IMessageBus messageBus
5956
+ )
5957
+ {
5958
+ if (
5959
+ !handlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5960
+ || erasedCache is not HandlerActionCache<TU> cache
5961
+ )
5962
+ {
5963
+ cache = new HandlerActionCache<TU>();
5964
+ handlers[priority] = cache;
5965
+ }
5966
+
5967
+ if (
5968
+ !cache.entries.TryGetValue(
5969
+ originalHandler,
5970
+ out HandlerActionCache<TU>.Entry entry
5971
+ )
5972
+ )
5973
+ {
5974
+ entry = new HandlerActionCache<TU>.Entry(augmentedHandler, 0);
5975
+ }
5976
+
5977
+ bool firstRegistration = entry.count == 0;
5978
+ entry = firstRegistration
5979
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1)
5980
+ : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5981
+
5982
+ cache.entries[originalHandler] = entry;
5983
+ cache.version++;
5984
+ TypedSlot<T> slot = FindPrioritySlot(handlers);
5985
+ if (slot != null)
5986
+ {
5987
+ slot.lastTouchTicks =
5988
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
5989
+ messageBus
5990
+ );
5991
+ }
5992
+ if (slot != null && !slot.orderedPriorities.Contains(priority))
5993
+ {
5994
+ slot.orderedPriorities.Add(priority);
5995
+ }
5996
+ if (firstRegistration && slot != null)
5997
+ {
5998
+ slot.liveCount++;
5999
+ }
6000
+
6001
+ Dictionary<int, IHandlerActionCache> localHandlers = handlers;
6002
+ TypedSlot<T> localSlot = slot;
6003
+ long localSlotVersion = slot?.version ?? 0;
6004
+ long localResetGeneration =
6005
+ global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
6006
+
6007
+ return () =>
6008
+ {
6009
+ if (
6010
+ !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
6011
+ messageBus,
6012
+ localResetGeneration
6013
+ )
6014
+ )
6015
+ {
6016
+ return;
6017
+ }
6018
+
6019
+ if (localSlot != null && localSlot.version != localSlotVersion)
6020
+ {
6021
+ return;
6022
+ }
6023
+
6024
+ if (
6025
+ !localHandlers.TryGetValue(
6026
+ priority,
6027
+ out IHandlerActionCache localErasedCache
6028
+ ) || localErasedCache is not HandlerActionCache<TU> localCache
6029
+ )
6030
+ {
6031
+ return;
6032
+ }
6033
+
6034
+ if (
6035
+ !localCache.entries.TryGetValue(
6036
+ originalHandler,
6037
+ out HandlerActionCache<TU>.Entry localEntry
6038
+ )
6039
+ )
6040
+ {
6041
+ return;
6042
+ }
6043
+
6044
+ localCache.version++;
6045
+
6046
+ deregistration?.Invoke();
6047
+ if (localSlot != null)
6048
+ {
6049
+ localSlot.lastTouchTicks =
6050
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
6051
+ messageBus
6052
+ );
6053
+ }
6054
+
6055
+ if (localEntry.count <= 1)
6056
+ {
6057
+ _ = localCache.entries.Remove(originalHandler);
6058
+ localCache.version++;
6059
+ if (localSlot != null)
6060
+ {
6061
+ localSlot.liveCount--;
6062
+ }
6063
+ // Intentionally DO NOT remove the priority key here to preserve
6064
+ // the cache handle during an in-flight emission.
6065
+ return;
6066
+ }
6067
+
6068
+ localEntry = new HandlerActionCache<TU>.Entry(
6069
+ localEntry.handler,
6070
+ localEntry.count - 1
6071
+ );
6072
+
6073
+ localCache.entries[originalHandler] = localEntry;
6074
+ };
6075
+ }
6076
+
4600
6077
  private static Action AddHandlerPreservingPriorityKey<TU>(
4601
6078
  ref Dictionary<int, HandlerActionCache<TU>> handlers,
4602
6079
  TU originalHandler,