com.wallstop-studios.dxmessaging 2.2.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +315 -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 +1129 -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 +46 -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 +11 -3
  26. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +81 -0
  27. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +11 -0
  28. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +429 -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 +11 -3
  32. package/Editor/CustomEditors.meta +2 -2
  33. package/Editor/DxMessagingEditorIdle.cs +62 -0
  34. package/Editor/DxMessagingEditorIdle.cs.meta +11 -0
  35. package/Editor/DxMessagingEditorInitializer.cs +112 -15
  36. package/Editor/DxMessagingEditorInitializer.cs.meta +11 -3
  37. package/Editor/DxMessagingEditorLog.cs +32 -0
  38. package/Editor/DxMessagingEditorLog.cs.meta +11 -0
  39. package/Editor/DxMessagingMenu.cs.meta +11 -11
  40. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +11 -11
  41. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +313 -0
  42. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +11 -0
  43. package/Editor/Settings/DxMessagingSettings.cs +261 -11
  44. package/Editor/Settings/DxMessagingSettings.cs.meta +11 -3
  45. package/Editor/Settings/DxMessagingSettingsProvider.cs +50 -33
  46. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +11 -3
  47. package/Editor/Settings.meta +2 -2
  48. package/Editor/SetupCscRsp.cs +406 -39
  49. package/Editor/SetupCscRsp.cs.meta +11 -3
  50. package/Editor/Testing/MessagingComponentEditorHarness.cs +2 -2
  51. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +11 -3
  52. package/Editor/Testing.meta +3 -3
  53. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef +14 -14
  54. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef.meta +7 -7
  55. package/Editor.meta +8 -8
  56. package/LICENSE.md +9 -9
  57. package/LICENSE.md.meta +7 -7
  58. package/README.md +940 -900
  59. package/README.md.meta +7 -7
  60. package/Runtime/AssemblyInfo.cs +4 -0
  61. package/Runtime/AssemblyInfo.cs.meta +11 -3
  62. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +11 -3
  63. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +11 -3
  64. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs +26 -0
  65. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +11 -0
  66. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs +2 -4
  67. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +11 -3
  68. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +11 -3
  69. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +11 -3
  70. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs +56 -0
  71. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs.meta +11 -0
  72. package/Runtime/Core/Attributes.meta +2 -2
  73. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs +195 -0
  74. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs.meta +11 -0
  75. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs +179 -0
  76. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs.meta +11 -0
  77. package/Runtime/Core/Configuration.meta +9 -0
  78. package/Runtime/Core/DataStructure/CyclicBuffer.cs +46 -28
  79. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +11 -3
  80. package/Runtime/Core/DataStructure.meta +2 -2
  81. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +11 -3
  82. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +11 -3
  83. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +11 -3
  84. package/Runtime/Core/Diagnostics.meta +2 -2
  85. package/Runtime/Core/DxMessagingStaticState.cs +19 -0
  86. package/Runtime/Core/DxMessagingStaticState.cs.meta +11 -11
  87. package/Runtime/Core/Extensions/EnumExtensions.cs +6 -5
  88. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +11 -3
  89. package/Runtime/Core/Extensions/IListExtensions.cs.meta +11 -3
  90. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +12 -12
  91. package/Runtime/Core/Extensions/MessageExtensions.cs +0 -60
  92. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +11 -11
  93. package/Runtime/Core/Extensions.meta +8 -8
  94. package/Runtime/Core/Helper/MessageCache.cs +32 -0
  95. package/Runtime/Core/Helper/MessageCache.cs.meta +11 -3
  96. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +11 -3
  97. package/Runtime/Core/Helper.meta +2 -2
  98. package/Runtime/Core/IMessage.cs +3 -3
  99. package/Runtime/Core/IMessage.cs.meta +11 -11
  100. package/Runtime/Core/InstanceId.cs +25 -1
  101. package/Runtime/Core/InstanceId.cs.meta +11 -11
  102. package/Runtime/Core/Internal/DxUnsafe.cs +60 -0
  103. package/Runtime/Core/Internal/DxUnsafe.cs.meta +11 -0
  104. package/Runtime/Core/Internal/FlatDispatch.cs +198 -0
  105. package/Runtime/Core/Internal/FlatDispatch.cs.meta +11 -0
  106. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs +38 -0
  107. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs.meta +11 -0
  108. package/Runtime/Core/Internal/TypedSlotIndex.cs +81 -0
  109. package/Runtime/Core/Internal/TypedSlotIndex.cs.meta +11 -0
  110. package/Runtime/Core/Internal/TypedSlots.cs +597 -0
  111. package/Runtime/Core/Internal/TypedSlots.cs.meta +11 -0
  112. package/Runtime/Core/Internal.meta +9 -0
  113. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +11 -11
  114. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +11 -11
  115. package/Runtime/Core/MessageBus/IMessageBus.cs +189 -15
  116. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +11 -11
  117. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +11 -11
  118. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs +1 -0
  119. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +11 -11
  120. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs +16 -0
  121. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs.meta +11 -0
  122. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs +40 -0
  123. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs.meta +11 -0
  124. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +719 -0
  125. package/Runtime/Core/MessageBus/Internal/BusSlots.cs.meta +11 -0
  126. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs +38 -0
  127. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs.meta +11 -0
  128. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs +20 -0
  129. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs.meta +11 -0
  130. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs +28 -0
  131. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs.meta +11 -0
  132. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs +48 -0
  133. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs.meta +11 -0
  134. package/Runtime/Core/MessageBus/Internal/ISweepable.cs +15 -0
  135. package/Runtime/Core/MessageBus/Internal/ISweepable.cs.meta +11 -0
  136. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs +222 -0
  137. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs.meta +11 -0
  138. package/Runtime/Core/MessageBus/Internal/SlotKey.cs +192 -0
  139. package/Runtime/Core/MessageBus/Internal/SlotKey.cs.meta +11 -0
  140. package/Runtime/Core/MessageBus/Internal.meta +9 -0
  141. package/Runtime/Core/MessageBus/MessageBus.cs +5366 -3838
  142. package/Runtime/Core/MessageBus/MessageBus.cs.meta +11 -11
  143. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +11 -11
  144. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs +187 -14
  145. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +11 -11
  146. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +11 -11
  147. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +11 -11
  148. package/Runtime/Core/MessageBus.meta +8 -8
  149. package/Runtime/Core/MessageHandler.cs +2399 -1042
  150. package/Runtime/Core/MessageHandler.cs.meta +11 -11
  151. package/Runtime/Core/MessageRegistrationHandle.cs.meta +11 -11
  152. package/Runtime/Core/MessageRegistrationToken.cs +429 -44
  153. package/Runtime/Core/MessageRegistrationToken.cs.meta +11 -11
  154. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +11 -3
  155. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +11 -11
  156. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +11 -11
  157. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +11 -11
  158. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +11 -3
  159. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +11 -11
  160. package/Runtime/Core/Messages/StringMessage.cs.meta +11 -3
  161. package/Runtime/Core/Messages.meta +8 -8
  162. package/Runtime/Core/MessagingDebug.cs.meta +11 -11
  163. package/Runtime/Core/Pooling/CollectionPool.cs +266 -0
  164. package/Runtime/Core/Pooling/CollectionPool.cs.meta +11 -0
  165. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs +30 -0
  166. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs.meta +11 -0
  167. package/Runtime/Core/Pooling/DxPools.cs +157 -0
  168. package/Runtime/Core/Pooling/DxPools.cs.meta +11 -0
  169. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs +106 -0
  170. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs.meta +11 -0
  171. package/Runtime/Core/Pooling/IDxMessagingClock.cs +18 -0
  172. package/Runtime/Core/Pooling/IDxMessagingClock.cs.meta +11 -0
  173. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs +55 -0
  174. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs.meta +11 -0
  175. package/Runtime/Core/Pooling/StopwatchClock.cs +27 -0
  176. package/Runtime/Core/Pooling/StopwatchClock.cs.meta +11 -0
  177. package/Runtime/Core/Pooling/UnityRealtimeClock.cs +31 -0
  178. package/Runtime/Core/Pooling/UnityRealtimeClock.cs.meta +11 -0
  179. package/Runtime/Core/Pooling.meta +9 -0
  180. package/Runtime/Core.meta +8 -8
  181. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +12 -12
  182. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +11 -11
  183. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +12 -12
  184. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +11 -3
  185. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs +73 -0
  186. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +11 -11
  187. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef +20 -20
  188. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef.meta +7 -7
  189. package/Runtime/Unity/Integrations/Reflex.meta +8 -8
  190. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +11 -3
  191. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs +109 -1
  192. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +11 -11
  193. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef +30 -30
  194. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef.meta +7 -7
  195. package/Runtime/Unity/Integrations/VContainer.meta +8 -8
  196. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +11 -3
  197. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef +30 -30
  198. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef.meta +7 -7
  199. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs +79 -1
  200. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +11 -11
  201. package/Runtime/Unity/Integrations/Zenject.meta +8 -8
  202. package/Runtime/Unity/Integrations.meta +8 -8
  203. package/Runtime/Unity/MessageAwareComponent.cs +74 -0
  204. package/Runtime/Unity/MessageAwareComponent.cs.meta +11 -11
  205. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +12 -12
  206. package/Runtime/Unity/MessagingComponent.cs +43 -10
  207. package/Runtime/Unity/MessagingComponent.cs.meta +11 -11
  208. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +12 -12
  209. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +12 -12
  210. package/Runtime/Unity.meta +8 -8
  211. package/Runtime/WallstopStudios.DxMessaging.asmdef +14 -14
  212. package/Runtime/WallstopStudios.DxMessaging.asmdef.meta +7 -7
  213. package/Runtime.meta +8 -8
  214. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab +98 -98
  215. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab.meta +7 -7
  216. package/Samples~/DI/Prefabs.meta +8 -8
  217. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset +14 -14
  218. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset.meta +8 -8
  219. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset +14 -14
  220. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset.meta +8 -8
  221. package/Samples~/DI/Providers.meta +8 -8
  222. package/Samples~/DI/README.md +51 -51
  223. package/Samples~/DI/README.md.meta +7 -7
  224. package/Samples~/DI/Reflex/SampleInstaller.cs +7 -0
  225. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +11 -11
  226. package/Samples~/DI/Reflex.meta +8 -8
  227. package/Samples~/DI/VContainer/SampleLifetimeScope.cs +6 -1
  228. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +11 -11
  229. package/Samples~/DI/VContainer.meta +8 -8
  230. package/Samples~/DI/Zenject/SampleInstaller.cs +8 -0
  231. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +11 -11
  232. package/Samples~/DI/Zenject.meta +8 -8
  233. package/Samples~/DI.meta +8 -8
  234. package/Samples~/Mini Combat/Boot.cs.meta +11 -11
  235. package/Samples~/Mini Combat/Enemy.cs.meta +11 -11
  236. package/Samples~/Mini Combat/Messages.cs.meta +11 -11
  237. package/Samples~/Mini Combat/Player.cs.meta +11 -11
  238. package/Samples~/Mini Combat/README.md +324 -323
  239. package/Samples~/Mini Combat/README.md.meta +7 -7
  240. package/Samples~/Mini Combat/UIOverlay.cs.meta +11 -11
  241. package/Samples~/Mini Combat/Walkthrough.md +430 -430
  242. package/Samples~/Mini Combat/Walkthrough.md.meta +7 -7
  243. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef +13 -13
  244. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef.meta +7 -7
  245. package/Samples~/Mini Combat.meta +8 -8
  246. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +11 -11
  247. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +11 -11
  248. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +11 -11
  249. package/Samples~/UI Buttons + Inspector/README.md +210 -209
  250. package/Samples~/UI Buttons + Inspector/README.md.meta +7 -7
  251. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +11 -11
  252. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef +13 -13
  253. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef.meta +7 -7
  254. package/Samples~/UI Buttons + Inspector.meta +8 -8
  255. package/SourceGenerators/Directory.Build.props +50 -3
  256. package/SourceGenerators/Directory.Build.props.meta +7 -7
  257. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs +96 -63
  258. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +11 -11
  259. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +745 -87
  260. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +11 -3
  261. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +39 -46
  262. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj.meta +7 -7
  263. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.meta +8 -8
  264. package/SourceGenerators/global.json +7 -0
  265. package/SourceGenerators/global.json.meta +7 -0
  266. package/SourceGenerators.meta +8 -8
  267. package/Third Party Notices.md +3 -3
  268. package/Third Party Notices.md.meta +7 -7
  269. package/package.json +102 -92
  270. 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,302 +37,6 @@ namespace DxMessaging.Core
34
37
  IComparable,
35
38
  IComparable<MessageHandler>
36
39
  {
37
- /// <summary>
38
- /// Pre-freezes this handler's broadcast post-processor caches for the given message type, source, and priority
39
- /// for the specified emission id, so registrations during the same emission are not observed.
40
- /// </summary>
41
- /// <typeparam name="T">Broadcast message type.</typeparam>
42
- /// <param name="source">Source instance id.</param>
43
- /// <param name="priority">Priority bucket to freeze.</param>
44
- /// <param name="emissionId">Current emission id.</param>
45
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
46
- internal void PrefreezeBroadcastPostProcessorsForEmission<T>(
47
- InstanceId source,
48
- int priority,
49
- long emissionId,
50
- IMessageBus messageBus
51
- )
52
- where T : IBroadcastMessage
53
- {
54
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
55
- {
56
- return;
57
- }
58
-
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
- }
85
- }
86
-
87
- /// <summary>
88
- /// Pre-freezes this handler's targeted post-processor caches for the given message type, target, and priority
89
- /// for the specified emission id, so registrations during the same emission are not observed.
90
- /// </summary>
91
- /// <typeparam name="T">Targeted message type.</typeparam>
92
- /// <param name="target">Target instance id.</param>
93
- /// <param name="priority">Priority bucket to freeze.</param>
94
- /// <param name="emissionId">Current emission id.</param>
95
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
96
- internal void PrefreezeTargetedPostProcessorsForEmission<T>(
97
- InstanceId target,
98
- int priority,
99
- long emissionId,
100
- IMessageBus messageBus
101
- )
102
- where T : ITargetedMessage
103
- {
104
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
105
- {
106
- return;
107
- }
108
-
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
- }
135
- }
136
-
137
- /// <summary>
138
- /// Pre-freezes this handler's targeted-without-targeting handler caches for the given message type and priority
139
- /// so that removals/additions during the same emission are not observed.
140
- /// </summary>
141
- internal void PrefreezeTargetedWithoutTargetingHandlersForEmission<T>(
142
- int priority,
143
- long emissionId,
144
- IMessageBus messageBus
145
- )
146
- where T : ITargetedMessage
147
- {
148
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
149
- {
150
- return;
151
- }
152
-
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
- }
174
- }
175
-
176
- /// <summary>
177
- /// Pre-freezes this handler's targeted-without-targeting post-processor caches for a given priority.
178
- /// </summary>
179
- internal void PrefreezeTargetedWithoutTargetingPostProcessorsForEmission<T>(
180
- int priority,
181
- long emissionId,
182
- IMessageBus messageBus
183
- )
184
- where T : ITargetedMessage
185
- {
186
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
187
- {
188
- return;
189
- }
190
-
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
- }
212
- }
213
-
214
- /// <summary>
215
- /// Pre-freezes this handler's untargeted post-processor caches for a given priority.
216
- /// </summary>
217
- internal void PrefreezeUntargetedPostProcessorsForEmission<T>(
218
- int priority,
219
- long emissionId,
220
- IMessageBus messageBus
221
- )
222
- where T : IUntargetedMessage
223
- {
224
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
225
- {
226
- return;
227
- }
228
-
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
- }
250
- }
251
-
252
- /// <summary>
253
- /// Pre-freezes this handler's broadcast-without-source post-processor caches for a given priority.
254
- /// </summary>
255
- internal void PrefreezeBroadcastWithoutSourcePostProcessorsForEmission<T>(
256
- int priority,
257
- long emissionId,
258
- IMessageBus messageBus
259
- )
260
- where T : IBroadcastMessage
261
- {
262
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
263
- {
264
- return;
265
- }
266
-
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
- }
288
- }
289
-
290
- /// <summary>
291
- /// Pre-freezes this handler's broadcast-without-source handler caches for the given message type and priority
292
- /// for the specified emission id, so removals during the same emission are not observed.
293
- /// </summary>
294
- /// <typeparam name="T">Broadcast message type.</typeparam>
295
- /// <param name="priority">Priority bucket to freeze.</param>
296
- /// <param name="emissionId">Current emission id.</param>
297
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
298
- internal void PrefreezeBroadcastWithoutSourceHandlersForEmission<T>(
299
- int priority,
300
- long emissionId,
301
- IMessageBus messageBus
302
- )
303
- where T : IBroadcastMessage
304
- {
305
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
306
- {
307
- return;
308
- }
309
-
310
- if (
311
- handler._fastBroadcastWithoutSourceHandlers != null
312
- && handler._fastBroadcastWithoutSourceHandlers.TryGetValue(
313
- priority,
314
- out HandlerActionCache<FastHandlerWithContext<T>> fastCache
315
- )
316
- )
317
- {
318
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(fastCache, emissionId);
319
- }
320
-
321
- if (
322
- handler._broadcastWithoutSourceHandlers != null
323
- && handler._broadcastWithoutSourceHandlers.TryGetValue(
324
- priority,
325
- out HandlerActionCache<Action<InstanceId, T>> cache
326
- )
327
- )
328
- {
329
- _ = TypedHandler<T>.GetOrAddNewHandlerStack(cache, emissionId);
330
- }
331
- }
332
-
333
40
  /// <summary>
334
41
  /// High-performance handler that receives the message by reference (no boxing/copies).
335
42
  /// </summary>
@@ -378,6 +85,19 @@ namespace DxMessaging.Core
378
85
  ResetStatics();
379
86
  }
380
87
 
88
+ /// <summary>
89
+ /// Reclaims empty slots and pooled collections owned by the current global message bus.
90
+ /// </summary>
91
+ /// <param name="force">
92
+ /// When true, ignores idle-age thresholds and drains shared pools to zero.
93
+ /// When false, only slots past the configured idle threshold are eligible.
94
+ /// </param>
95
+ /// <returns>Counts describing what was reclaimed.</returns>
96
+ public static IMessageBus.TrimResult TrimAll(bool force = false)
97
+ {
98
+ return MessageBus.Trim(force);
99
+ }
100
+
381
101
  /// <summary>
382
102
  /// Replaces the global <see cref="Core.MessageBus.MessageBus"/> instance returned by <see cref="MessageBus"/>.
383
103
  /// </summary>
@@ -928,19 +648,19 @@ namespace DxMessaging.Core
928
648
  return;
929
649
  }
930
650
 
931
- if (handler._globalUntargetedFastHandlers != null)
651
+ HandlerActionCache<FastHandler<IUntargetedMessage>> fastCache = handler.GetGlobalCache<
652
+ FastHandler<IUntargetedMessage>
653
+ >(TypedGlobalSlotIndex.UntargetedFast);
654
+ if (fastCache != null)
932
655
  {
933
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
934
- handler._globalUntargetedFastHandlers,
935
- emissionId
936
- );
656
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
937
657
  }
938
- if (handler._globalUntargetedHandlers != null)
658
+ HandlerActionCache<Action<IUntargetedMessage>> cache = handler.GetGlobalCache<
659
+ Action<IUntargetedMessage>
660
+ >(TypedGlobalSlotIndex.UntargetedDefault);
661
+ if (cache != null)
939
662
  {
940
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
941
- handler._globalUntargetedHandlers,
942
- emissionId
943
- );
663
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
944
664
  }
945
665
  }
946
666
 
@@ -954,19 +674,20 @@ namespace DxMessaging.Core
954
674
  return;
955
675
  }
956
676
 
957
- if (handler._globalTargetedFastHandlers != null)
958
- {
959
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
960
- handler._globalTargetedFastHandlers,
961
- emissionId
677
+ HandlerActionCache<FastHandlerWithContext<ITargetedMessage>> fastCache =
678
+ handler.GetGlobalCache<FastHandlerWithContext<ITargetedMessage>>(
679
+ TypedGlobalSlotIndex.TargetedFast
962
680
  );
681
+ if (fastCache != null)
682
+ {
683
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
963
684
  }
964
- if (handler._globalTargetedHandlers != null)
685
+ HandlerActionCache<Action<InstanceId, ITargetedMessage>> cache = handler.GetGlobalCache<
686
+ Action<InstanceId, ITargetedMessage>
687
+ >(TypedGlobalSlotIndex.TargetedDefault);
688
+ if (cache != null)
965
689
  {
966
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
967
- handler._globalTargetedHandlers,
968
- emissionId
969
- );
690
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
970
691
  }
971
692
  }
972
693
 
@@ -980,19 +701,21 @@ namespace DxMessaging.Core
980
701
  return;
981
702
  }
982
703
 
983
- if (handler._globalBroadcastFastHandlers != null)
984
- {
985
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
986
- handler._globalBroadcastFastHandlers,
987
- emissionId
704
+ HandlerActionCache<FastHandlerWithContext<IBroadcastMessage>> fastCache =
705
+ handler.GetGlobalCache<FastHandlerWithContext<IBroadcastMessage>>(
706
+ TypedGlobalSlotIndex.BroadcastFast
988
707
  );
989
- }
990
- if (handler._globalBroadcastHandlers != null)
708
+ if (fastCache != null)
991
709
  {
992
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(
993
- handler._globalBroadcastHandlers,
994
- emissionId
710
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
711
+ }
712
+ HandlerActionCache<Action<InstanceId, IBroadcastMessage>> cache =
713
+ handler.GetGlobalCache<Action<InstanceId, IBroadcastMessage>>(
714
+ TypedGlobalSlotIndex.BroadcastDefault
995
715
  );
716
+ if (cache != null)
717
+ {
718
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
996
719
  }
997
720
  }
998
721
 
@@ -1071,25 +794,28 @@ namespace DxMessaging.Core
1071
794
  Action untargetedDeregistration = typedHandler.AddGlobalUntargetedHandler(
1072
795
  originalUntargetedMessageHandler,
1073
796
  untargetedMessageHandler,
1074
- NullDeregistration
797
+ NullDeregistration,
798
+ messageBus
1075
799
  );
1076
800
  Action targetedDeregistration = typedHandler.AddGlobalTargetedHandler(
1077
801
  originalTargetedMessageHandler,
1078
802
  targetedMessageHandler,
1079
- NullDeregistration
803
+ NullDeregistration,
804
+ messageBus
1080
805
  );
1081
806
  Action broadcastDeregistration = typedHandler.AddGlobalBroadcastHandler(
1082
807
  originalBroadcastMessageHandler,
1083
808
  broadcastMessageHandler,
1084
- NullDeregistration
809
+ NullDeregistration,
810
+ messageBus
1085
811
  );
1086
812
 
1087
813
  return () =>
1088
814
  {
815
+ messageBusDeregistration?.Invoke();
1089
816
  untargetedDeregistration();
1090
817
  targetedDeregistration();
1091
818
  broadcastDeregistration();
1092
- messageBusDeregistration?.Invoke();
1093
819
  };
1094
820
 
1095
821
  void NullDeregistration()
@@ -1123,25 +849,28 @@ namespace DxMessaging.Core
1123
849
  Action untargetedDeregistration = typedHandler.AddGlobalUntargetedHandler(
1124
850
  originalUntargetedMessageHandler,
1125
851
  untargetedMessageHandler,
1126
- NullDeregistration
852
+ NullDeregistration,
853
+ messageBus
1127
854
  );
1128
855
  Action targetedDeregistration = typedHandler.AddGlobalTargetedHandler(
1129
856
  originalTargetedMessageHandler,
1130
857
  targetedMessageHandler,
1131
- NullDeregistration
858
+ NullDeregistration,
859
+ messageBus
1132
860
  );
1133
861
  Action broadcastDeregistration = typedHandler.AddGlobalBroadcastHandler(
1134
862
  originalBroadcastMessageHandler,
1135
863
  broadcastMessageHandler,
1136
- NullDeregistration
864
+ NullDeregistration,
865
+ messageBus
1137
866
  );
1138
867
 
1139
868
  return () =>
1140
869
  {
870
+ messageBusDeregistration?.Invoke();
1141
871
  untargetedDeregistration();
1142
872
  targetedDeregistration();
1143
873
  broadcastDeregistration();
1144
- messageBusDeregistration?.Invoke();
1145
874
  };
1146
875
 
1147
876
  void NullDeregistration()
@@ -1181,7 +910,7 @@ namespace DxMessaging.Core
1181
910
  messageHandler,
1182
911
  messageBusDeregistration,
1183
912
  priority,
1184
- messageBus.EmissionId
913
+ messageBus
1185
914
  );
1186
915
  }
1187
916
 
@@ -1216,7 +945,7 @@ namespace DxMessaging.Core
1216
945
  messageHandler,
1217
946
  messageBusDeregistration,
1218
947
  priority,
1219
- messageBus.EmissionId
948
+ messageBus
1220
949
  );
1221
950
  }
1222
951
 
@@ -1251,7 +980,7 @@ namespace DxMessaging.Core
1251
980
  messageHandler,
1252
981
  messageBusDeregistration,
1253
982
  priority,
1254
- messageBus.EmissionId
983
+ messageBus
1255
984
  );
1256
985
  }
1257
986
 
@@ -1286,7 +1015,7 @@ namespace DxMessaging.Core
1286
1015
  messageHandler,
1287
1016
  messageBusDeregistration,
1288
1017
  priority,
1289
- messageBus.EmissionId
1018
+ messageBus
1290
1019
  );
1291
1020
  }
1292
1021
 
@@ -1318,7 +1047,7 @@ namespace DxMessaging.Core
1318
1047
  messageHandler,
1319
1048
  messageBusDeregistration,
1320
1049
  priority,
1321
- messageBus.EmissionId
1050
+ messageBus
1322
1051
  );
1323
1052
  }
1324
1053
 
@@ -1350,7 +1079,7 @@ namespace DxMessaging.Core
1350
1079
  messageHandler,
1351
1080
  messageBusDeregistration,
1352
1081
  priority,
1353
- messageBus.EmissionId
1082
+ messageBus
1354
1083
  );
1355
1084
  }
1356
1085
 
@@ -1381,7 +1110,7 @@ namespace DxMessaging.Core
1381
1110
  messageHandler,
1382
1111
  messageBusDeregistration,
1383
1112
  priority,
1384
- messageBus.EmissionId
1113
+ messageBus
1385
1114
  );
1386
1115
  }
1387
1116
 
@@ -1412,7 +1141,7 @@ namespace DxMessaging.Core
1412
1141
  messageHandler,
1413
1142
  messageBusDeregistration,
1414
1143
  priority,
1415
- messageBus.EmissionId
1144
+ messageBus
1416
1145
  );
1417
1146
  }
1418
1147
 
@@ -1443,7 +1172,7 @@ namespace DxMessaging.Core
1443
1172
  messageHandler,
1444
1173
  messageBusDeregistration,
1445
1174
  priority,
1446
- messageBus.EmissionId
1175
+ messageBus
1447
1176
  );
1448
1177
  }
1449
1178
 
@@ -1474,7 +1203,7 @@ namespace DxMessaging.Core
1474
1203
  messageHandler,
1475
1204
  messageBusDeregistration,
1476
1205
  priority,
1477
- messageBus.EmissionId
1206
+ messageBus
1478
1207
  );
1479
1208
  }
1480
1209
 
@@ -1505,7 +1234,7 @@ namespace DxMessaging.Core
1505
1234
  messageHandler,
1506
1235
  messageBusDeregistration,
1507
1236
  priority,
1508
- messageBus.EmissionId
1237
+ messageBus
1509
1238
  );
1510
1239
  }
1511
1240
 
@@ -1536,7 +1265,7 @@ namespace DxMessaging.Core
1536
1265
  messageHandler,
1537
1266
  messageBusDeregistration,
1538
1267
  priority,
1539
- messageBus.EmissionId
1268
+ messageBus
1540
1269
  );
1541
1270
  }
1542
1271
 
@@ -1572,7 +1301,7 @@ namespace DxMessaging.Core
1572
1301
  messageHandler,
1573
1302
  messageBusDeregistration,
1574
1303
  priority,
1575
- messageBus.EmissionId
1304
+ messageBus
1576
1305
  );
1577
1306
  }
1578
1307
 
@@ -1607,7 +1336,7 @@ namespace DxMessaging.Core
1607
1336
  messageHandler,
1608
1337
  messageBusDeregistration,
1609
1338
  priority,
1610
- messageBus.EmissionId
1339
+ messageBus
1611
1340
  );
1612
1341
  }
1613
1342
 
@@ -1638,7 +1367,7 @@ namespace DxMessaging.Core
1638
1367
  messageHandler,
1639
1368
  messageBusDeregistration,
1640
1369
  priority,
1641
- messageBus.EmissionId
1370
+ messageBus
1642
1371
  );
1643
1372
  }
1644
1373
 
@@ -1669,7 +1398,7 @@ namespace DxMessaging.Core
1669
1398
  messageHandler,
1670
1399
  messageBusDeregistration,
1671
1400
  priority,
1672
- messageBus.EmissionId
1401
+ messageBus
1673
1402
  );
1674
1403
  }
1675
1404
 
@@ -1704,7 +1433,7 @@ namespace DxMessaging.Core
1704
1433
  messageHandler,
1705
1434
  messageBusDeregistration,
1706
1435
  priority,
1707
- messageBus.EmissionId
1436
+ messageBus
1708
1437
  );
1709
1438
  }
1710
1439
 
@@ -1739,7 +1468,7 @@ namespace DxMessaging.Core
1739
1468
  messageHandler,
1740
1469
  messageBusDeregistration,
1741
1470
  priority,
1742
- messageBus.EmissionId
1471
+ messageBus
1743
1472
  );
1744
1473
  }
1745
1474
 
@@ -1771,7 +1500,7 @@ namespace DxMessaging.Core
1771
1500
  messageHandler,
1772
1501
  messageBusDeregistration,
1773
1502
  priority,
1774
- messageBus.EmissionId
1503
+ messageBus
1775
1504
  );
1776
1505
  }
1777
1506
 
@@ -1803,7 +1532,7 @@ namespace DxMessaging.Core
1803
1532
  messageHandler,
1804
1533
  messageBusDeregistration,
1805
1534
  priority,
1806
- messageBus.EmissionId
1535
+ messageBus
1807
1536
  );
1808
1537
  }
1809
1538
 
@@ -1956,7 +1685,7 @@ namespace DxMessaging.Core
1956
1685
  MessageCache<object> handlersByType = _handlersByTypeByMessageBus[messageBusIndex];
1957
1686
  if (handlersByType.TryGetValue<T>(out object untypedHandler))
1958
1687
  {
1959
- return Unsafe.As<TypedHandler<T>>(untypedHandler);
1688
+ return DxUnsafe.As<TypedHandler<T>>(untypedHandler);
1960
1689
  }
1961
1690
 
1962
1691
  TypedHandler<T> typedHandler = new();
@@ -1988,7 +1717,7 @@ namespace DxMessaging.Core
1988
1717
  .TryGetValue<T>(out object untypedHandler)
1989
1718
  )
1990
1719
  {
1991
- existingTypedHandler = Unsafe.As<TypedHandler<T>>(untypedHandler);
1720
+ existingTypedHandler = DxUnsafe.As<TypedHandler<T>>(untypedHandler);
1992
1721
  return true;
1993
1722
  }
1994
1723
 
@@ -1996,675 +1725,931 @@ namespace DxMessaging.Core
1996
1725
  return false;
1997
1726
  }
1998
1727
 
1999
- internal int GetUntargetedPostProcessingPrefreezeCount<T>(
2000
- IMessageBus messageBus,
2001
- int priority
2002
- )
2003
- where T : IMessage
1728
+ /// <summary>
1729
+ /// Resets empty typed-handler slots associated with
1730
+ /// <paramref name="messageBus"/>. The eviction layer calls through
1731
+ /// this erased surface after bus-side slots prove idle and empty.
1732
+ /// </summary>
1733
+ /// <param name="messageBus">
1734
+ /// Bus whose typed-handler cache should be swept. Null resolves to
1735
+ /// this handler's default bus.
1736
+ /// </param>
1737
+ /// <returns>Number of typed or typed-global slots reset.</returns>
1738
+ internal int ResetEmptyTypedSlotsForSweep(IMessageBus messageBus = null)
2004
1739
  {
2005
- if (
2006
- !GetHandlerForType(messageBus, out TypedHandler<T> handler)
2007
- || handler._untargetedPostProcessingFastHandlers == null
2008
- )
1740
+ messageBus = ResolveMessageBus(messageBus);
1741
+ int messageBusIndex = messageBus.RegisteredGlobalSequentialIndex;
1742
+ if (messageBusIndex < 0 || _handlersByTypeByMessageBus.Count <= messageBusIndex)
2009
1743
  {
2010
1744
  return 0;
2011
1745
  }
2012
1746
 
2013
- if (
2014
- handler._untargetedPostProcessingFastHandlers.TryGetValue(
2015
- priority,
2016
- out HandlerActionCache<FastHandler<T>> cache
2017
- )
2018
- )
1747
+ int resetCount = 0;
1748
+ MessageCache<object> handlersByType = _handlersByTypeByMessageBus[messageBusIndex];
1749
+ foreach (object untypedHandler in _handlersByTypeByMessageBus[messageBusIndex])
2019
1750
  {
2020
- return cache.prefreezeInvocationCount;
1751
+ if (untypedHandler is ITypedHandlerSlotSweeper sweeper)
1752
+ {
1753
+ resetCount += sweeper.ResetEmptySlotsForSweep();
1754
+ if (sweeper.MarkedForOuterRemoval)
1755
+ {
1756
+ handlersByType.RemoveAtIndex(sweeper.MessageTypeIndex);
1757
+ }
1758
+ }
2021
1759
  }
2022
1760
 
2023
- return 0;
1761
+ return resetCount;
2024
1762
  }
2025
1763
 
2026
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2027
- internal UntargetedDispatchLink<T> GetOrCreateUntargetedDispatchLink<T>(
2028
- IMessageBus messageBus
2029
- )
2030
- where T : IMessage
1764
+ internal int ResetAllTypedSlotsForBusReset(IMessageBus messageBus = null)
2031
1765
  {
2032
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2033
- return typedHandler.GetOrCreateUntargetedLink();
1766
+ messageBus = ResolveMessageBus(messageBus);
1767
+ int messageBusIndex = messageBus.RegisteredGlobalSequentialIndex;
1768
+ if (messageBusIndex < 0 || _handlersByTypeByMessageBus.Count <= messageBusIndex)
1769
+ {
1770
+ return 0;
1771
+ }
1772
+
1773
+ int resetCount = 0;
1774
+ foreach (object untypedHandler in _handlersByTypeByMessageBus[messageBusIndex])
1775
+ {
1776
+ if (untypedHandler is ITypedHandlerSlotSweeper sweeper)
1777
+ {
1778
+ resetCount += sweeper.ResetAllSlotsForBusReset();
1779
+ }
1780
+ }
1781
+
1782
+ return resetCount;
2034
1783
  }
2035
1784
 
2036
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2037
- internal UntargetedPostDispatchLink<T> GetOrCreateUntargetedPostDispatchLink<T>(
2038
- IMessageBus messageBus
2039
- )
2040
- where T : IMessage
1785
+ internal int CountEmptyTypedSlotsForSweep(IMessageBus messageBus = null)
2041
1786
  {
2042
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2043
- return typedHandler.GetOrCreateUntargetedPostLink();
1787
+ messageBus = ResolveMessageBus(messageBus);
1788
+ int messageBusIndex = messageBus.RegisteredGlobalSequentialIndex;
1789
+ if (messageBusIndex < 0 || _handlersByTypeByMessageBus.Count <= messageBusIndex)
1790
+ {
1791
+ return 0;
1792
+ }
1793
+
1794
+ int count = 0;
1795
+ foreach (object untypedHandler in _handlersByTypeByMessageBus[messageBusIndex])
1796
+ {
1797
+ if (untypedHandler is ITypedHandlerSlotSweeper sweeper)
1798
+ {
1799
+ count += sweeper.CountEmptySlotsForSweep();
1800
+ }
1801
+ }
1802
+
1803
+ return count;
2044
1804
  }
2045
1805
 
2046
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2047
- internal TargetedDispatchLink<T> GetOrCreateTargetedDispatchLink<T>(IMessageBus messageBus)
2048
- where T : IMessage
1806
+ internal bool HasTypedHandlersForBus(IMessageBus messageBus = null)
2049
1807
  {
2050
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2051
- return typedHandler.GetOrCreateTargetedLink();
1808
+ messageBus = ResolveMessageBus(messageBus);
1809
+ int messageBusIndex = messageBus.RegisteredGlobalSequentialIndex;
1810
+ if (messageBusIndex < 0 || _handlersByTypeByMessageBus.Count <= messageBusIndex)
1811
+ {
1812
+ return false;
1813
+ }
1814
+
1815
+ foreach (object untypedHandler in _handlersByTypeByMessageBus[messageBusIndex])
1816
+ {
1817
+ if (untypedHandler != null)
1818
+ {
1819
+ return true;
1820
+ }
1821
+ }
1822
+
1823
+ return false;
2052
1824
  }
2053
1825
 
2054
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2055
- internal TargetedPostDispatchLink<T> GetOrCreateTargetedPostDispatchLink<T>(
2056
- IMessageBus messageBus
1826
+ /// <summary>
1827
+ /// Counts the flat-dispatch entries this handler contributes to a
1828
+ /// non-context-keyed slot (untargeted handle/post) at the given
1829
+ /// priority: one entry per unique registered delegate (fast plus
1830
+ /// default). Build-time only; called by MessageBus.BuildFlatDispatch.
1831
+ /// </summary>
1832
+ internal int CountFlatHandlers<T>(
1833
+ IMessageBus messageBus,
1834
+ int priority,
1835
+ int fastIndex,
1836
+ int defaultIndex
2057
1837
  )
2058
1838
  where T : IMessage
2059
1839
  {
2060
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2061
- return typedHandler.GetOrCreateTargetedPostLink();
1840
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1841
+ {
1842
+ return 0;
1843
+ }
1844
+
1845
+ return CountFlatDelegates<FastHandler<T>>(
1846
+ typedHandler.GetPriorityHandlers(fastIndex),
1847
+ priority
1848
+ )
1849
+ + CountFlatDelegates<Action<T>>(
1850
+ typedHandler.GetPriorityHandlers(defaultIndex),
1851
+ priority
1852
+ );
2062
1853
  }
2063
1854
 
2064
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2065
- internal TargetedWithoutTargetingDispatchLink<T> GetOrCreateTargetedWithoutTargetingDispatchLink<T>(
2066
- IMessageBus messageBus
1855
+ /// <summary>
1856
+ /// Counts the flat-dispatch entries this handler contributes to a
1857
+ /// context-keyed slot (Default-variant targeted/broadcast handle/post)
1858
+ /// for the given context and priority. Build-time only.
1859
+ /// </summary>
1860
+ internal int CountContextFlatHandlers<T>(
1861
+ IMessageBus messageBus,
1862
+ InstanceId context,
1863
+ int priority,
1864
+ int fastIndex,
1865
+ int defaultIndex
2067
1866
  )
2068
1867
  where T : IMessage
2069
1868
  {
2070
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2071
- return typedHandler.GetOrCreateTargetedWithoutTargetingLink();
1869
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1870
+ {
1871
+ return 0;
1872
+ }
1873
+
1874
+ return CountFlatDelegates<FastHandler<T>>(
1875
+ GetContextPriorityHandlers(typedHandler, fastIndex, context),
1876
+ priority
1877
+ )
1878
+ + CountFlatDelegates<Action<T>>(
1879
+ GetContextPriorityHandlers(typedHandler, defaultIndex, context),
1880
+ priority
1881
+ );
2072
1882
  }
2073
1883
 
2074
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2075
- internal TargetedWithoutTargetingPostDispatchLink<T> GetOrCreateTargetedWithoutTargetingPostDispatchLink<T>(
2076
- IMessageBus messageBus
1884
+ /// <summary>
1885
+ /// Counts the flat-dispatch entries this handler contributes to a
1886
+ /// WithoutContext targeted/broadcast slot (whose delegates receive the
1887
+ /// routing InstanceId) at the given priority. Build-time only.
1888
+ /// </summary>
1889
+ internal int CountWithContextFlatHandlers<T>(
1890
+ IMessageBus messageBus,
1891
+ int priority,
1892
+ int fastIndex,
1893
+ int defaultIndex
2077
1894
  )
2078
1895
  where T : IMessage
2079
1896
  {
2080
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2081
- return typedHandler.GetOrCreateTargetedWithoutTargetingPostLink();
1897
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1898
+ {
1899
+ return 0;
1900
+ }
1901
+
1902
+ return CountFlatDelegates<FastHandlerWithContext<T>>(
1903
+ typedHandler.GetPriorityHandlers(fastIndex),
1904
+ priority
1905
+ )
1906
+ + CountFlatDelegates<Action<InstanceId, T>>(
1907
+ typedHandler.GetPriorityHandlers(defaultIndex),
1908
+ priority
1909
+ );
2082
1910
  }
2083
1911
 
2084
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2085
- internal BroadcastDispatchLink<T> GetOrCreateBroadcastDispatchLink<T>(
2086
- IMessageBus messageBus
1912
+ /// <summary>
1913
+ /// Writes this handler's resolved flat-dispatch entries for a
1914
+ /// non-context-keyed FastHandler-shaped slot into
1915
+ /// <paramref name="target"/> starting at <paramref name="writeIndex"/>:
1916
+ /// all fast entries in registration order, then all default entries in
1917
+ /// registration order, matching the legacy link path's
1918
+ /// fast-before-default contract. Fast entries resolve to the augmented
1919
+ /// FastHandler delegate itself; default entries resolve to the
1920
+ /// FastHandler adapter created at registration time
1921
+ /// (Entry.flatInvoker), so this method allocates nothing.
1922
+ /// Returns the next write index.
1923
+ /// </summary>
1924
+ internal int FillFlatHandlers<T>(
1925
+ IMessageBus messageBus,
1926
+ int priority,
1927
+ int fastIndex,
1928
+ int defaultIndex,
1929
+ FlatDispatchEntry<T>[] target,
1930
+ int writeIndex
2087
1931
  )
2088
1932
  where T : IMessage
2089
1933
  {
2090
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2091
- return typedHandler.GetOrCreateBroadcastLink();
1934
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1935
+ {
1936
+ return writeIndex;
1937
+ }
1938
+
1939
+ writeIndex = FillFastFlatEntries(
1940
+ typedHandler.GetPriorityHandlers(fastIndex),
1941
+ priority,
1942
+ target,
1943
+ writeIndex
1944
+ );
1945
+ return FillDefaultFlatEntries(
1946
+ typedHandler.GetPriorityHandlers(defaultIndex),
1947
+ priority,
1948
+ target,
1949
+ writeIndex
1950
+ );
2092
1951
  }
2093
1952
 
2094
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2095
- internal BroadcastPostDispatchLink<T> GetOrCreateBroadcastPostDispatchLink<T>(
2096
- IMessageBus messageBus
1953
+ /// <summary>
1954
+ /// Context-keyed sibling of <see cref="FillFlatHandlers{T}"/> for the
1955
+ /// Default-variant targeted/broadcast slots: resolves the per-context
1956
+ /// priority map first, then fills fast entries followed by default
1957
+ /// entries in registration order. Returns the next write index.
1958
+ /// </summary>
1959
+ internal int FillContextFlatHandlers<T>(
1960
+ IMessageBus messageBus,
1961
+ InstanceId context,
1962
+ int priority,
1963
+ int fastIndex,
1964
+ int defaultIndex,
1965
+ FlatDispatchEntry<T>[] target,
1966
+ int writeIndex
2097
1967
  )
2098
1968
  where T : IMessage
2099
1969
  {
2100
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2101
- return typedHandler.GetOrCreateBroadcastPostLink();
1970
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1971
+ {
1972
+ return writeIndex;
1973
+ }
1974
+
1975
+ writeIndex = FillFastFlatEntries(
1976
+ GetContextPriorityHandlers(typedHandler, fastIndex, context),
1977
+ priority,
1978
+ target,
1979
+ writeIndex
1980
+ );
1981
+ return FillDefaultFlatEntries(
1982
+ GetContextPriorityHandlers(typedHandler, defaultIndex, context),
1983
+ priority,
1984
+ target,
1985
+ writeIndex
1986
+ );
2102
1987
  }
2103
1988
 
2104
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2105
- internal BroadcastWithoutSourceDispatchLink<T> GetOrCreateBroadcastWithoutSourceDispatchLink<T>(
2106
- IMessageBus messageBus
1989
+ /// <summary>
1990
+ /// WithoutContext sibling of <see cref="FillFlatHandlers{T}"/> for the
1991
+ /// targeted/broadcast slots whose delegates receive the routing
1992
+ /// InstanceId: fills fast (FastHandlerWithContext) entries followed by
1993
+ /// default (Action&lt;InstanceId, T&gt;, via the registration-time
1994
+ /// FastHandlerWithContext adapter) entries in registration order.
1995
+ /// Returns the next write index.
1996
+ /// </summary>
1997
+ internal int FillWithContextFlatHandlers<T>(
1998
+ IMessageBus messageBus,
1999
+ int priority,
2000
+ int fastIndex,
2001
+ int defaultIndex,
2002
+ ContextFlatDispatchEntry<T>[] target,
2003
+ int writeIndex
2107
2004
  )
2108
2005
  where T : IMessage
2109
2006
  {
2110
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2111
- return typedHandler.GetOrCreateBroadcastWithoutSourceLink();
2007
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
2008
+ {
2009
+ return writeIndex;
2010
+ }
2011
+
2012
+ writeIndex = FillFastWithContextFlatEntries(
2013
+ typedHandler.GetPriorityHandlers(fastIndex),
2014
+ priority,
2015
+ target,
2016
+ writeIndex
2017
+ );
2018
+ return FillDefaultWithContextFlatEntries(
2019
+ typedHandler.GetPriorityHandlers(defaultIndex),
2020
+ priority,
2021
+ target,
2022
+ writeIndex
2023
+ );
2112
2024
  }
2113
2025
 
2114
2026
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2115
- internal BroadcastWithoutSourcePostDispatchLink<T> GetOrCreateBroadcastWithoutSourcePostDispatchLink<T>(
2116
- IMessageBus messageBus
2027
+ private static Dictionary<int, IHandlerActionCache> GetContextPriorityHandlers<T>(
2028
+ TypedHandler<T> typedHandler,
2029
+ int slotIndex,
2030
+ InstanceId context
2117
2031
  )
2118
2032
  where T : IMessage
2119
2033
  {
2120
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2121
- return typedHandler.GetOrCreateBroadcastWithoutSourcePostLink();
2034
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> byContext =
2035
+ typedHandler.GetContextHandlers(slotIndex);
2036
+ if (
2037
+ byContext != null
2038
+ && byContext.TryGetValue(
2039
+ context,
2040
+ out Dictionary<int, IHandlerActionCache> byPriority
2041
+ )
2042
+ )
2043
+ {
2044
+ return byPriority;
2045
+ }
2046
+
2047
+ return null;
2122
2048
  }
2123
2049
 
2124
- internal sealed class HandlerActionCache<T>
2050
+ private static int CountFlatDelegates<TDelegate>(
2051
+ Dictionary<int, IHandlerActionCache> byPriority,
2052
+ int priority
2053
+ )
2125
2054
  {
2126
- internal readonly struct Entry
2055
+ if (
2056
+ byPriority != null
2057
+ && byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2058
+ && erased is HandlerActionCache<TDelegate> cache
2059
+ )
2127
2060
  {
2128
- /// <summary>
2129
- /// Initializes an entry used to track handler invocation counts.
2130
- /// </summary>
2131
- /// <param name="handler">Handler delegate being tracked.</param>
2132
- /// <param name="count">Number of times the handler has been cached.</param>
2133
- public Entry(T handler, int count)
2134
- {
2135
- this.handler = handler;
2136
- this.count = count;
2137
- }
2138
-
2139
- public readonly T handler;
2140
- public readonly int count;
2061
+ return cache.insertionOrder.Count;
2141
2062
  }
2142
2063
 
2143
- public readonly Dictionary<T, Entry> entries = new();
2144
- public readonly List<T> cache = new();
2145
- public long version;
2146
- public long lastSeenVersion = -1;
2147
- public long lastSeenEmissionId;
2148
- internal int prefreezeInvocationCount;
2064
+ return 0;
2149
2065
  }
2150
2066
 
2151
- internal sealed class UntargetedDispatchLink<T>
2067
+ private int FillFastFlatEntries<T>(
2068
+ Dictionary<int, IHandlerActionCache> byPriority,
2069
+ int priority,
2070
+ FlatDispatchEntry<T>[] target,
2071
+ int writeIndex
2072
+ )
2152
2073
  where T : IMessage
2153
2074
  {
2154
- private readonly TypedHandler<T> typedHandler;
2155
-
2156
- internal UntargetedDispatchLink(TypedHandler<T> typedHandler)
2075
+ if (
2076
+ byPriority == null
2077
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2078
+ || erased is not HandlerActionCache<FastHandler<T>> cache
2079
+ )
2157
2080
  {
2158
- this.typedHandler = typedHandler;
2081
+ return writeIndex;
2159
2082
  }
2160
2083
 
2161
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2162
- internal void Invoke(
2163
- MessageHandler messageHandler,
2164
- ref T message,
2165
- int priority,
2166
- long emissionId
2167
- )
2084
+ List<FastHandler<T>> ordered = cache.insertionOrder;
2085
+ int orderedCount = ordered.Count;
2086
+ for (int i = 0; i < orderedCount; ++i)
2168
2087
  {
2169
- if (!messageHandler.active)
2088
+ if (
2089
+ cache.entries.TryGetValue(
2090
+ ordered[i],
2091
+ out HandlerActionCache<FastHandler<T>>.Entry entry
2092
+ )
2093
+ )
2170
2094
  {
2171
- return;
2095
+ target[writeIndex++] = new FlatDispatchEntry<T>(this, entry.handler);
2172
2096
  }
2173
-
2174
- typedHandler.HandleUntargeted(ref message, priority, emissionId);
2175
2097
  }
2098
+
2099
+ return writeIndex;
2176
2100
  }
2177
2101
 
2178
- internal sealed class UntargetedPostDispatchLink<TMessage>
2179
- where TMessage : IMessage
2102
+ private int FillDefaultFlatEntries<T>(
2103
+ Dictionary<int, IHandlerActionCache> byPriority,
2104
+ int priority,
2105
+ FlatDispatchEntry<T>[] target,
2106
+ int writeIndex
2107
+ )
2108
+ where T : IMessage
2180
2109
  {
2181
- private readonly TypedHandler<TMessage> typedHandler;
2182
-
2183
- internal UntargetedPostDispatchLink(TypedHandler<TMessage> typedHandler)
2110
+ if (
2111
+ byPriority == null
2112
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2113
+ || erased is not HandlerActionCache<Action<T>> cache
2114
+ )
2184
2115
  {
2185
- this.typedHandler = typedHandler;
2116
+ return writeIndex;
2186
2117
  }
2187
2118
 
2188
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2189
- internal void Invoke(
2190
- MessageHandler messageHandler,
2191
- ref TMessage message,
2192
- int priority,
2193
- long emissionId
2194
- )
2119
+ List<Action<T>> ordered = cache.insertionOrder;
2120
+ int orderedCount = ordered.Count;
2121
+ for (int i = 0; i < orderedCount; ++i)
2195
2122
  {
2196
- if (!messageHandler.active)
2123
+ if (
2124
+ !cache.entries.TryGetValue(
2125
+ ordered[i],
2126
+ out HandlerActionCache<Action<T>>.Entry entry
2127
+ )
2128
+ )
2197
2129
  {
2198
- return;
2130
+ continue;
2199
2131
  }
2200
2132
 
2201
- typedHandler.HandleUntargetedPostProcessing(ref message, priority, emissionId);
2133
+ // Every default registration path for the flattened slots
2134
+ // supplies the adapter at registration time (AddUntargetedHandler,
2135
+ // AddTargetedHandler, AddSourcedBroadcastHandler, and their
2136
+ // post-processor siblings). The type test doubles as a null
2137
+ // guard; a missing adapter would indicate a new registration
2138
+ // path that forgot to provide one.
2139
+ if (entry.flatInvoker is FastHandler<T> invoker)
2140
+ {
2141
+ target[writeIndex++] = new FlatDispatchEntry<T>(this, invoker);
2142
+ }
2143
+ else
2144
+ {
2145
+ System.Diagnostics.Debug.Assert(
2146
+ false,
2147
+ "Default registration is missing its FastHandler flat invoker; "
2148
+ + "every Add*Handler/Add*PostProcessor default path must adapt "
2149
+ + "the augmented handler at registration time."
2150
+ );
2151
+ }
2202
2152
  }
2153
+
2154
+ return writeIndex;
2203
2155
  }
2204
2156
 
2205
- internal sealed class TargetedDispatchLink<TMessage>
2206
- where TMessage : IMessage
2157
+ private int FillFastWithContextFlatEntries<T>(
2158
+ Dictionary<int, IHandlerActionCache> byPriority,
2159
+ int priority,
2160
+ ContextFlatDispatchEntry<T>[] target,
2161
+ int writeIndex
2162
+ )
2163
+ where T : IMessage
2207
2164
  {
2208
- private readonly TypedHandler<TMessage> typedHandler;
2209
-
2210
- internal TargetedDispatchLink(TypedHandler<TMessage> typedHandler)
2165
+ if (
2166
+ byPriority == null
2167
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2168
+ || erased is not HandlerActionCache<FastHandlerWithContext<T>> cache
2169
+ )
2211
2170
  {
2212
- this.typedHandler = typedHandler;
2171
+ return writeIndex;
2213
2172
  }
2214
2173
 
2215
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2216
- internal void Invoke(
2217
- MessageHandler messageHandler,
2218
- ref InstanceId target,
2219
- ref TMessage message,
2220
- int priority,
2221
- long emissionId
2222
- )
2174
+ List<FastHandlerWithContext<T>> ordered = cache.insertionOrder;
2175
+ int orderedCount = ordered.Count;
2176
+ for (int i = 0; i < orderedCount; ++i)
2223
2177
  {
2224
- typedHandler.HandleTargeted(ref target, ref message, priority, emissionId);
2178
+ if (
2179
+ cache.entries.TryGetValue(
2180
+ ordered[i],
2181
+ out HandlerActionCache<FastHandlerWithContext<T>>.Entry entry
2182
+ )
2183
+ )
2184
+ {
2185
+ target[writeIndex++] = new ContextFlatDispatchEntry<T>(this, entry.handler);
2186
+ }
2225
2187
  }
2188
+
2189
+ return writeIndex;
2226
2190
  }
2227
2191
 
2228
- internal sealed class TargetedPostDispatchLink<TMessage>
2229
- where TMessage : IMessage
2192
+ private int FillDefaultWithContextFlatEntries<T>(
2193
+ Dictionary<int, IHandlerActionCache> byPriority,
2194
+ int priority,
2195
+ ContextFlatDispatchEntry<T>[] target,
2196
+ int writeIndex
2197
+ )
2198
+ where T : IMessage
2230
2199
  {
2231
- private readonly TypedHandler<TMessage> typedHandler;
2232
-
2233
- internal TargetedPostDispatchLink(TypedHandler<TMessage> typedHandler)
2200
+ if (
2201
+ byPriority == null
2202
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2203
+ || erased is not HandlerActionCache<Action<InstanceId, T>> cache
2204
+ )
2234
2205
  {
2235
- this.typedHandler = typedHandler;
2206
+ return writeIndex;
2236
2207
  }
2237
2208
 
2238
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2239
- internal void Invoke(
2240
- MessageHandler messageHandler,
2241
- ref InstanceId target,
2242
- ref TMessage message,
2243
- int priority,
2244
- long emissionId
2245
- )
2209
+ List<Action<InstanceId, T>> ordered = cache.insertionOrder;
2210
+ int orderedCount = ordered.Count;
2211
+ for (int i = 0; i < orderedCount; ++i)
2246
2212
  {
2247
- typedHandler.HandleTargetedPostProcessing(
2248
- ref target,
2249
- ref message,
2250
- priority,
2251
- emissionId
2252
- );
2213
+ if (
2214
+ !cache.entries.TryGetValue(
2215
+ ordered[i],
2216
+ out HandlerActionCache<Action<InstanceId, T>>.Entry entry
2217
+ )
2218
+ )
2219
+ {
2220
+ continue;
2221
+ }
2222
+
2223
+ // See FillDefaultFlatEntries: the adapter is created once at
2224
+ // registration time (AddTargetedWithoutTargetingHandler,
2225
+ // AddSourcedBroadcastWithoutSourceHandler, and their
2226
+ // post-processor siblings).
2227
+ if (entry.flatInvoker is FastHandlerWithContext<T> invoker)
2228
+ {
2229
+ target[writeIndex++] = new ContextFlatDispatchEntry<T>(this, invoker);
2230
+ }
2231
+ else
2232
+ {
2233
+ System.Diagnostics.Debug.Assert(
2234
+ false,
2235
+ "Default with-context registration is missing its "
2236
+ + "FastHandlerWithContext flat invoker; every without-context "
2237
+ + "Add* default path must adapt the augmented handler at "
2238
+ + "registration time."
2239
+ );
2240
+ }
2253
2241
  }
2242
+
2243
+ return writeIndex;
2254
2244
  }
2255
2245
 
2256
- internal sealed class TargetedWithoutTargetingDispatchLink<TMessage>
2257
- where TMessage : IMessage
2246
+ internal sealed class HandlerActionCache<T> : DxMessaging.Core.Internal.IHandlerActionCache
2258
2247
  {
2259
- private readonly TypedHandler<TMessage> typedHandler;
2260
-
2261
- internal TargetedWithoutTargetingDispatchLink(TypedHandler<TMessage> typedHandler)
2248
+ // Uses outer T as a field type -- reflection callers must close
2249
+ // via MakeGenericType(outer.GetGenericArguments()) before passing
2250
+ // this type to Activator.CreateInstance. See
2251
+ // Tests/Editor/Contract/ReflectionHelpers.cs::CloseNestedGeneric.
2252
+ internal readonly struct Entry
2262
2253
  {
2263
- this.typedHandler = typedHandler;
2264
- }
2254
+ /// <summary>
2255
+ /// Initializes an entry used to track handler invocation counts.
2256
+ /// </summary>
2257
+ /// <param name="handler">Handler delegate being tracked.</param>
2258
+ /// <param name="count">Number of times the handler has been cached.</param>
2259
+ public Entry(T handler, int count)
2260
+ : this(handler, count, null) { }
2265
2261
 
2266
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2267
- internal void Invoke(
2268
- MessageHandler messageHandler,
2269
- ref InstanceId target,
2270
- ref TMessage message,
2271
- int priority,
2272
- long emissionId
2273
- )
2274
- {
2275
- typedHandler.HandleTargetedWithoutTargeting(
2276
- ref target,
2277
- ref message,
2278
- priority,
2279
- emissionId
2280
- );
2262
+ /// <summary>
2263
+ /// Initializes an entry that additionally carries a pre-resolved
2264
+ /// flat-dispatch invoker (see <see cref="flatInvoker"/>).
2265
+ /// </summary>
2266
+ /// <param name="handler">Handler delegate being tracked.</param>
2267
+ /// <param name="count">Number of times the handler has been cached.</param>
2268
+ /// <param name="flatInvoker">Pre-resolved flat-dispatch invoker, if any.</param>
2269
+ public Entry(T handler, int count, object flatInvoker)
2270
+ {
2271
+ this.handler = handler;
2272
+ this.count = count;
2273
+ this.flatInvoker = flatInvoker;
2274
+ }
2275
+
2276
+ public readonly T handler;
2277
+ public readonly int count;
2278
+
2279
+ // Pre-resolved invoker consumed by the bus-side flat dispatch
2280
+ // snapshot (MessageBus.BuildMessageFlatDispatch). For
2281
+ // default Action<TMessage> registrations this holds a
2282
+ // FastHandler<TMessage> adapter wrapping the AUGMENTED
2283
+ // handler, created exactly ONCE at registration time so
2284
+ // snapshot rebuilds never allocate closures. For delegate
2285
+ // shapes the flat path does not consume (fast handlers, which
2286
+ // already ARE the invoker, and every targeted/broadcast
2287
+ // shape) this stays null. Refcount increments and decrements
2288
+ // preserve the first registration's invoker, mirroring the
2289
+ // first-augmented-handler-wins semantics of `handler`.
2290
+ public readonly object flatInvoker;
2281
2291
  }
2282
- }
2283
2292
 
2284
- internal sealed class TargetedWithoutTargetingPostDispatchLink<TMessage>
2285
- where TMessage : IMessage
2286
- {
2287
- private readonly TypedHandler<TMessage> typedHandler;
2293
+ public readonly Dictionary<T, Entry> entries = new();
2288
2294
 
2289
- internal TargetedWithoutTargetingPostDispatchLink(TypedHandler<TMessage> typedHandler)
2295
+ // Original-handler keys in first-registration order. Dictionary
2296
+ // enumeration order is NOT stable across Remove/Add churn (.NET
2297
+ // reuses freed slots LIFO), so dispatch snapshots are rebuilt from
2298
+ // this list instead of from <see cref="entries"/> to honor the
2299
+ // documented "same priority uses registration order" contract.
2300
+ // Invariants: contains exactly the keys of <see cref="entries"/>;
2301
+ // a key is appended on its FIRST registration only (refcount
2302
+ // increments do not move it) and removed when its refcount drops
2303
+ // to zero. Maintained exclusively by the AddHandler* family and
2304
+ // <see cref="DxMessaging.Core.Internal.IHandlerActionCache.Reset"/>.
2305
+ public readonly List<T> insertionOrder = new();
2306
+ public readonly List<T> cache = new();
2307
+ public long version;
2308
+ public long lastSeenVersion = -1;
2309
+ public long lastSeenEmissionId = -1;
2310
+
2311
+ /// <summary>Monotonic version field, read-only on the interface surface.</summary>
2312
+ long DxMessaging.Core.Internal.IHandlerActionCache.Version
2290
2313
  {
2291
- this.typedHandler = typedHandler;
2314
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2315
+ get => version;
2292
2316
  }
2293
2317
 
2294
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2295
- internal void Invoke(
2296
- MessageHandler messageHandler,
2297
- ref InstanceId target,
2298
- ref TMessage message,
2299
- int priority,
2300
- long emissionId
2301
- )
2318
+ /// <summary>Most recent dispatcher-observed version; mutable through the staged dispatch path.</summary>
2319
+ long DxMessaging.Core.Internal.IHandlerActionCache.LastSeenVersion
2302
2320
  {
2303
- typedHandler.HandleTargetedWithoutTargetingPostProcessing(
2304
- ref target,
2305
- ref message,
2306
- priority,
2307
- emissionId
2308
- );
2321
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2322
+ get => lastSeenVersion;
2323
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2324
+ set => lastSeenVersion = value;
2309
2325
  }
2310
- }
2311
-
2312
- internal sealed class BroadcastDispatchLink<TMessage>
2313
- where TMessage : IMessage
2314
- {
2315
- private readonly TypedHandler<TMessage> typedHandler;
2316
2326
 
2317
- internal BroadcastDispatchLink(TypedHandler<TMessage> typedHandler)
2327
+ /// <summary>Most recent dispatcher-observed bus emission id.</summary>
2328
+ long DxMessaging.Core.Internal.IHandlerActionCache.LastSeenEmissionId
2318
2329
  {
2319
- this.typedHandler = typedHandler;
2330
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2331
+ get => lastSeenEmissionId;
2332
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2333
+ set => lastSeenEmissionId = value;
2320
2334
  }
2321
2335
 
2322
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2323
- internal void Invoke(
2324
- MessageHandler messageHandler,
2325
- ref InstanceId source,
2326
- ref TMessage message,
2327
- int priority,
2328
- long emissionId
2329
- )
2336
+ /// <summary>True iff the entries dictionary holds zero handlers.</summary>
2337
+ bool DxMessaging.Core.Internal.IHandlerActionCache.IsEmpty
2330
2338
  {
2331
- typedHandler.HandleSourcedBroadcast(ref source, ref message, priority, emissionId);
2339
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2340
+ get => entries.Count == 0;
2341
+ }
2342
+
2343
+ /// <summary>
2344
+ /// Eviction-driven full clear; bumps <see cref="version"/> as the LAST step
2345
+ /// so captured dispatch closures observe invalidation.
2346
+ /// </summary>
2347
+ void DxMessaging.Core.Internal.IHandlerActionCache.Reset()
2348
+ {
2349
+ entries.Clear();
2350
+ insertionOrder.Clear();
2351
+ cache.Clear();
2352
+ lastSeenVersion = -1;
2353
+ lastSeenEmissionId = -1;
2354
+ unchecked
2355
+ {
2356
+ ++version;
2357
+ }
2332
2358
  }
2333
2359
  }
2334
2360
 
2335
- internal sealed class BroadcastPostDispatchLink<TMessage>
2336
- where TMessage : IMessage
2361
+ /// <summary>
2362
+ /// One-size-fits-all wrapper around all possible Messaging sinks for a particular MessageHandler & MessageType.
2363
+ /// </summary>
2364
+ /// <typeparam name="T">Message type that this Handler exists to serve.</typeparam>
2365
+ internal sealed class TypedHandler<T> : ITypedHandlerSlotSweeper
2366
+ where T : IMessage
2337
2367
  {
2338
- private readonly TypedHandler<TMessage> typedHandler;
2368
+ // Typed storage: 20 typed slots + 6 global slots. The legacy
2369
+ // named fields were deleted so new handler variants must pick an
2370
+ // explicit axis-indexed slot.
2371
+ internal readonly TypedSlot<T>[] _slots = new TypedSlot<T>[TypedSlotIndex.Length];
2372
+ internal readonly TypedGlobalSlot[] _globalSlots = new TypedGlobalSlot[
2373
+ TypedGlobalSlotIndex.Length
2374
+ ];
2339
2375
 
2340
- internal BroadcastPostDispatchLink(TypedHandler<TMessage> typedHandler)
2376
+ // Constructor exists solely so the [Conditional("DEBUG")]
2377
+ // validator below runs at construction time. In Release builds
2378
+ // the Conditional attribute strips the call site, leaving an
2379
+ // empty constructor body that the JIT collapses to the
2380
+ // equivalent of the implicit default. Mirrors the
2381
+ // MessageBus.ValidateSinkArrays() pattern.
2382
+ internal TypedHandler()
2341
2383
  {
2342
- this.typedHandler = typedHandler;
2384
+ ValidateSlotArrays();
2343
2385
  }
2344
2386
 
2345
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2346
- internal void Invoke(
2347
- MessageHandler messageHandler,
2348
- ref InstanceId source,
2349
- ref TMessage message,
2350
- int priority,
2351
- long emissionId
2352
- )
2387
+ internal bool _markedForOuterRemoval;
2388
+
2389
+ int ITypedHandlerSlotSweeper.MessageTypeIndex
2353
2390
  {
2354
- typedHandler.HandleSourcedBroadcastPostProcessing(
2355
- ref source,
2356
- ref message,
2357
- priority,
2358
- emissionId
2359
- );
2391
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2392
+ get => MessageHelperIndexer<T>.SequentialId;
2360
2393
  }
2361
- }
2362
2394
 
2363
- internal sealed class BroadcastWithoutSourceDispatchLink<TMessage>
2364
- where TMessage : IMessage
2365
- {
2366
- private readonly TypedHandler<TMessage> typedHandler;
2395
+ bool ITypedHandlerSlotSweeper.MarkedForOuterRemoval
2396
+ {
2397
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2398
+ get => _markedForOuterRemoval;
2399
+ }
2367
2400
 
2368
- internal BroadcastWithoutSourceDispatchLink(TypedHandler<TMessage> typedHandler)
2401
+ [Conditional("DEBUG")]
2402
+ private void ValidateSlotArrays()
2369
2403
  {
2370
- this.typedHandler = typedHandler;
2404
+ if (_slots.Length != TypedSlotIndex.Length)
2405
+ {
2406
+ throw new InvalidOperationException(
2407
+ $"_slots length is {_slots.Length} but TypedSlotIndex.Length is {TypedSlotIndex.Length}."
2408
+ );
2409
+ }
2410
+ if (_globalSlots.Length != TypedGlobalSlotIndex.Length)
2411
+ {
2412
+ throw new InvalidOperationException(
2413
+ $"_globalSlots length is {_globalSlots.Length} but TypedGlobalSlotIndex.Length is {TypedGlobalSlotIndex.Length}."
2414
+ );
2415
+ }
2416
+ // Lazy registration writers update the slot arrays; this assertion still
2417
+ // holds at construction (slots populate on first register,
2418
+ // not on construction). The invariant flips meaning -- not
2419
+ // the message -- when writers land.
2420
+ for (int i = 0; i < _slots.Length; ++i)
2421
+ {
2422
+ if (_slots[i] != null)
2423
+ {
2424
+ throw new InvalidOperationException(
2425
+ $"_slots[{i}] is non-null at construction; expected null per TypedSlotIndex because slots populate lazily on first registration."
2426
+ );
2427
+ }
2428
+ }
2429
+ for (int i = 0; i < _globalSlots.Length; ++i)
2430
+ {
2431
+ if (_globalSlots[i] != null)
2432
+ {
2433
+ throw new InvalidOperationException(
2434
+ $"_globalSlots[{i}] is non-null at construction; expected null per TypedGlobalSlotIndex because slots populate lazily on first registration."
2435
+ );
2436
+ }
2437
+ }
2371
2438
  }
2372
2439
 
2373
2440
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2374
- internal void Invoke(
2375
- MessageHandler messageHandler,
2376
- ref InstanceId source,
2377
- ref TMessage message,
2378
- int priority,
2379
- long emissionId
2380
- )
2441
+ private TypedSlot<T> GetOrCreateSlot(int index, bool requiresContext)
2381
2442
  {
2382
- typedHandler.HandleSourcedBroadcastWithoutSource(
2383
- ref source,
2384
- ref message,
2385
- priority,
2386
- emissionId
2387
- );
2388
- }
2389
- }
2443
+ TypedSlot<T> slot = _slots[index];
2444
+ if (slot == null)
2445
+ {
2446
+ slot = new TypedSlot<T>(requiresContext);
2447
+ _slots[index] = slot;
2448
+ }
2390
2449
 
2391
- internal sealed class BroadcastWithoutSourcePostDispatchLink<TMessage>
2392
- where TMessage : IMessage
2393
- {
2394
- private readonly TypedHandler<TMessage> typedHandler;
2450
+ return slot;
2451
+ }
2395
2452
 
2396
- internal BroadcastWithoutSourcePostDispatchLink(TypedHandler<TMessage> typedHandler)
2453
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2454
+ internal Dictionary<int, IHandlerActionCache> GetOrCreatePriorityHandlers(
2455
+ int index,
2456
+ bool requiresContext
2457
+ )
2397
2458
  {
2398
- this.typedHandler = typedHandler;
2459
+ return GetOrCreateSlot(index, requiresContext).byPriority;
2399
2460
  }
2400
2461
 
2401
2462
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2402
- internal void Invoke(
2403
- MessageHandler messageHandler,
2404
- ref InstanceId source,
2405
- ref TMessage message,
2406
- int priority,
2407
- long emissionId
2408
- )
2463
+ internal Dictionary<int, IHandlerActionCache> GetPriorityHandlers(int index)
2409
2464
  {
2410
- typedHandler.HandleBroadcastWithoutSourcePostProcessing(
2411
- ref source,
2412
- ref message,
2413
- priority,
2414
- emissionId
2415
- );
2465
+ return _slots[index]?.byPriority;
2416
2466
  }
2417
- }
2418
2467
 
2419
- /// <summary>
2420
- /// One-size-fits-all wrapper around all possible Messaging sinks for a particular MessageHandler & MessageType.
2421
- /// </summary>
2422
- /// <typeparam name="T">Message type that this Handler exists to serve.</typeparam>
2423
- internal sealed class TypedHandler<T>
2424
- where T : IMessage
2425
- {
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;
2468
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2464
2469
  internal Dictionary<
2465
2470
  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;
2474
-
2475
- internal HandlerActionCache<
2476
- Action<InstanceId, IBroadcastMessage>
2477
- > _globalBroadcastHandlers;
2478
-
2479
- internal HandlerActionCache<
2480
- FastHandler<IUntargetedMessage>
2481
- > _globalUntargetedFastHandlers;
2482
-
2483
- internal HandlerActionCache<
2484
- FastHandlerWithContext<ITargetedMessage>
2485
- > _globalTargetedFastHandlers;
2471
+ Dictionary<int, IHandlerActionCache>
2472
+ > GetOrCreateContextHandlers(int index)
2473
+ {
2474
+ TypedSlot<T> slot = GetOrCreateSlot(index, requiresContext: true);
2475
+ slot.byContext ??= DxPools.TypedHandlerContextDicts.Rent();
2476
+ return slot.byContext;
2477
+ }
2486
2478
 
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;
2479
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2518
2480
  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;
2481
+ InstanceId,
2482
+ Dictionary<int, IHandlerActionCache>
2483
+ > GetContextHandlers(int index)
2484
+ {
2485
+ return _slots[index]?.byContext;
2486
+ }
2532
2487
 
2533
2488
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2534
- internal UntargetedDispatchLink<T> GetOrCreateUntargetedLink()
2489
+ internal TypedGlobalSlot GetOrCreateGlobalSlot(int index)
2535
2490
  {
2536
- UntargetedDispatchLink<T> link = _untargetedLink;
2537
- if (link == null)
2491
+ TypedGlobalSlot slot = _globalSlots[index];
2492
+ if (slot == null)
2538
2493
  {
2539
- link = new UntargetedDispatchLink<T>(this);
2540
- _untargetedLink = link;
2494
+ slot = new TypedGlobalSlot();
2495
+ _globalSlots[index] = slot;
2541
2496
  }
2542
2497
 
2543
- return link;
2498
+ return slot;
2544
2499
  }
2545
2500
 
2546
2501
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2547
- internal UntargetedPostDispatchLink<T> GetOrCreateUntargetedPostLink()
2502
+ internal HandlerActionCache<TU> GetGlobalCache<TU>(int index)
2548
2503
  {
2549
- UntargetedPostDispatchLink<T> link =
2550
- _untargetedPostLink as UntargetedPostDispatchLink<T>;
2551
- if (link == null)
2552
- {
2553
- link = new UntargetedPostDispatchLink<T>(this);
2554
- _untargetedPostLink = link;
2555
- }
2556
-
2557
- return link;
2504
+ return _globalSlots[index]?.cache as HandlerActionCache<TU>;
2558
2505
  }
2559
2506
 
2560
2507
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2561
- internal TargetedDispatchLink<T> GetOrCreateTargetedLink()
2508
+ private TypedSlot<T> FindPrioritySlot(Dictionary<int, IHandlerActionCache> handlers)
2562
2509
  {
2563
- TargetedDispatchLink<T> link = _targetedLink as TargetedDispatchLink<T>;
2564
- if (link == null)
2510
+ for (int i = 0; i < _slots.Length; ++i)
2565
2511
  {
2566
- link = new TargetedDispatchLink<T>(this);
2567
- _targetedLink = link;
2512
+ TypedSlot<T> slot = _slots[i];
2513
+ if (slot != null && ReferenceEquals(slot.byPriority, handlers))
2514
+ {
2515
+ return slot;
2516
+ }
2568
2517
  }
2569
2518
 
2570
- return link;
2519
+ return null;
2571
2520
  }
2572
2521
 
2573
2522
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2574
- internal TargetedPostDispatchLink<T> GetOrCreateTargetedPostLink()
2523
+ private TypedSlot<T> FindContextSlot(
2524
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext
2525
+ )
2575
2526
  {
2576
- TargetedPostDispatchLink<T> link = _targetedPostLink as TargetedPostDispatchLink<T>;
2577
- if (link == null)
2527
+ for (int i = 0; i < _slots.Length; ++i)
2578
2528
  {
2579
- link = new TargetedPostDispatchLink<T>(this);
2580
- _targetedPostLink = link;
2529
+ TypedSlot<T> slot = _slots[i];
2530
+ if (slot != null && ReferenceEquals(slot.byContext, handlersByContext))
2531
+ {
2532
+ return slot;
2533
+ }
2581
2534
  }
2582
2535
 
2583
- return link;
2536
+ return null;
2584
2537
  }
2585
2538
 
2586
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2587
- internal TargetedWithoutTargetingDispatchLink<T> GetOrCreateTargetedWithoutTargetingLink()
2539
+ int ITypedHandlerSlotSweeper.ResetEmptySlotsForSweep()
2588
2540
  {
2589
- TargetedWithoutTargetingDispatchLink<T> link =
2590
- _targetedWithoutTargetingLink as TargetedWithoutTargetingDispatchLink<T>;
2591
- if (link == null)
2541
+ _markedForOuterRemoval = false;
2542
+ int resetCount = 0;
2543
+ for (int i = 0; i < _slots.Length; ++i)
2544
+ {
2545
+ TypedSlot<T> slot = _slots[i];
2546
+ if (slot != null && slot.IsEmpty)
2547
+ {
2548
+ slot.Reset();
2549
+ _slots[i] = null;
2550
+ resetCount++;
2551
+ }
2552
+ }
2553
+
2554
+ for (int i = 0; i < _globalSlots.Length; ++i)
2592
2555
  {
2593
- link = new TargetedWithoutTargetingDispatchLink<T>(this);
2594
- _targetedWithoutTargetingLink = link;
2556
+ TypedGlobalSlot slot = _globalSlots[i];
2557
+ if (slot != null && slot.IsEmpty)
2558
+ {
2559
+ slot.Reset();
2560
+ _globalSlots[i] = null;
2561
+ resetCount++;
2562
+ }
2595
2563
  }
2596
2564
 
2597
- return link;
2565
+ MarkForOuterRemovalIfEmpty();
2566
+ return resetCount;
2598
2567
  }
2599
2568
 
2600
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2601
- internal TargetedWithoutTargetingPostDispatchLink<T> GetOrCreateTargetedWithoutTargetingPostLink()
2569
+ int ITypedHandlerSlotSweeper.ResetAllSlotsForBusReset()
2602
2570
  {
2603
- TargetedWithoutTargetingPostDispatchLink<T> link =
2604
- _targetedWithoutTargetingPostLink
2605
- as TargetedWithoutTargetingPostDispatchLink<T>;
2606
- if (link == null)
2571
+ _markedForOuterRemoval = false;
2572
+ int resetCount = 0;
2573
+ for (int i = 0; i < _slots.Length; ++i)
2607
2574
  {
2608
- link = new TargetedWithoutTargetingPostDispatchLink<T>(this);
2609
- _targetedWithoutTargetingPostLink = link;
2575
+ TypedSlot<T> slot = _slots[i];
2576
+ if (slot != null)
2577
+ {
2578
+ slot.Reset();
2579
+ _slots[i] = null;
2580
+ resetCount++;
2581
+ }
2610
2582
  }
2611
2583
 
2612
- return link;
2584
+ for (int i = 0; i < _globalSlots.Length; ++i)
2585
+ {
2586
+ TypedGlobalSlot slot = _globalSlots[i];
2587
+ if (slot != null)
2588
+ {
2589
+ slot.Reset();
2590
+ _globalSlots[i] = null;
2591
+ resetCount++;
2592
+ }
2593
+ }
2594
+
2595
+ return resetCount;
2613
2596
  }
2614
2597
 
2615
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2616
- internal BroadcastDispatchLink<T> GetOrCreateBroadcastLink()
2598
+ int ITypedHandlerSlotSweeper.CountEmptySlotsForSweep()
2617
2599
  {
2618
- BroadcastDispatchLink<T> link = _broadcastLink as BroadcastDispatchLink<T>;
2619
- if (link == null)
2600
+ int count = 0;
2601
+ for (int i = 0; i < _slots.Length; ++i)
2620
2602
  {
2621
- link = new BroadcastDispatchLink<T>(this);
2622
- _broadcastLink = link;
2603
+ TypedSlot<T> slot = _slots[i];
2604
+ if (slot != null && slot.IsEmpty)
2605
+ {
2606
+ count++;
2607
+ }
2608
+ }
2609
+
2610
+ for (int i = 0; i < _globalSlots.Length; ++i)
2611
+ {
2612
+ TypedGlobalSlot slot = _globalSlots[i];
2613
+ if (slot != null && slot.IsEmpty)
2614
+ {
2615
+ count++;
2616
+ }
2623
2617
  }
2624
2618
 
2625
- return link;
2619
+ return count;
2626
2620
  }
2627
2621
 
2628
2622
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2629
- internal BroadcastPostDispatchLink<T> GetOrCreateBroadcastPostLink()
2623
+ private void MarkForOuterRemovalIfEmpty()
2630
2624
  {
2631
- BroadcastPostDispatchLink<T> link =
2632
- _broadcastPostLink as BroadcastPostDispatchLink<T>;
2633
- if (link == null)
2625
+ if (HasLiveSlots())
2634
2626
  {
2635
- link = new BroadcastPostDispatchLink<T>(this);
2636
- _broadcastPostLink = link;
2627
+ return;
2637
2628
  }
2638
2629
 
2639
- return link;
2630
+ _markedForOuterRemoval = true;
2640
2631
  }
2641
2632
 
2642
2633
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2643
- internal BroadcastWithoutSourceDispatchLink<T> GetOrCreateBroadcastWithoutSourceLink()
2634
+ private bool HasLiveSlots()
2644
2635
  {
2645
- BroadcastWithoutSourceDispatchLink<T> link =
2646
- _broadcastWithoutSourceLink as BroadcastWithoutSourceDispatchLink<T>;
2647
- if (link == null)
2636
+ for (int i = 0; i < _slots.Length; ++i)
2648
2637
  {
2649
- link = new BroadcastWithoutSourceDispatchLink<T>(this);
2650
- _broadcastWithoutSourceLink = link;
2638
+ if (_slots[i] != null)
2639
+ {
2640
+ return true;
2641
+ }
2651
2642
  }
2652
2643
 
2653
- return link;
2654
- }
2655
-
2656
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2657
- internal BroadcastWithoutSourcePostDispatchLink<T> GetOrCreateBroadcastWithoutSourcePostLink()
2658
- {
2659
- BroadcastWithoutSourcePostDispatchLink<T> link =
2660
- _broadcastWithoutSourcePostLink as BroadcastWithoutSourcePostDispatchLink<T>;
2661
- if (link == null)
2644
+ for (int i = 0; i < _globalSlots.Length; ++i)
2662
2645
  {
2663
- link = new BroadcastWithoutSourcePostDispatchLink<T>(this);
2664
- _broadcastWithoutSourcePostLink = link;
2646
+ if (_globalSlots[i] != null)
2647
+ {
2648
+ return true;
2649
+ }
2665
2650
  }
2666
2651
 
2667
- return link;
2652
+ return false;
2668
2653
  }
2669
2654
 
2670
2655
  /// <summary>
@@ -2674,19 +2659,18 @@ namespace DxMessaging.Core
2674
2659
  /// <param name="priority">Priority at which to run the handlers.</param>
2675
2660
  public void HandleUntargeted(ref T message, int priority, long emissionId)
2676
2661
  {
2677
- PrefreezeHandlersForEmission(
2678
- _untargetedPostProcessingFastHandlers,
2662
+ RunFastHandlers(
2663
+ GetPriorityHandlers(TypedSlotIndex.UntargetedHandleFast),
2664
+ ref message,
2679
2665
  priority,
2680
2666
  emissionId
2681
2667
  );
2682
- PrefreezeHandlersForEmission(
2683
- _untargetedPostProcessingHandlers,
2668
+ RunHandlers(
2669
+ GetPriorityHandlers(TypedSlotIndex.UntargetedHandleDefault),
2670
+ ref message,
2684
2671
  priority,
2685
2672
  emissionId
2686
2673
  );
2687
-
2688
- RunFastHandlers(_untargetedFastHandlers, ref message, priority, emissionId);
2689
- RunHandlers(_untargetedHandlers, ref message, priority, emissionId);
2690
2674
  }
2691
2675
 
2692
2676
  /// <summary>
@@ -2704,14 +2688,14 @@ namespace DxMessaging.Core
2704
2688
  {
2705
2689
  RunFastHandlersWithContext(
2706
2690
  ref target,
2707
- _targetedFastHandlers,
2691
+ GetContextHandlers(TypedSlotIndex.TargetedHandleFast),
2708
2692
  ref message,
2709
2693
  priority,
2710
2694
  emissionId
2711
2695
  );
2712
2696
  RunHandlersWithContext(
2713
2697
  ref target,
2714
- _targetedHandlers,
2698
+ GetContextHandlers(TypedSlotIndex.TargetedHandleDefault),
2715
2699
  ref message,
2716
2700
  priority,
2717
2701
  emissionId
@@ -2733,14 +2717,14 @@ namespace DxMessaging.Core
2733
2717
  {
2734
2718
  RunFastHandlers(
2735
2719
  ref target,
2736
- _fastTargetedWithoutTargetingHandlers,
2720
+ GetPriorityHandlers(TypedSlotIndex.TargetedHandleWithoutContextFast),
2737
2721
  ref message,
2738
2722
  priority,
2739
2723
  emissionId
2740
2724
  );
2741
2725
  RunHandlers(
2742
2726
  ref target,
2743
- _targetedWithoutTargetingHandlers,
2727
+ GetPriorityHandlers(TypedSlotIndex.TargetedHandleWithoutContext),
2744
2728
  ref message,
2745
2729
  priority,
2746
2730
  emissionId
@@ -2762,14 +2746,14 @@ namespace DxMessaging.Core
2762
2746
  {
2763
2747
  RunFastHandlersWithContext(
2764
2748
  ref source,
2765
- _broadcastFastHandlers,
2749
+ GetContextHandlers(TypedSlotIndex.BroadcastHandleFast),
2766
2750
  ref message,
2767
2751
  priority,
2768
2752
  emissionId
2769
2753
  );
2770
2754
  RunHandlersWithContext(
2771
2755
  ref source,
2772
- _broadcastHandlers,
2756
+ GetContextHandlers(TypedSlotIndex.BroadcastHandleDefault),
2773
2757
  ref message,
2774
2758
  priority,
2775
2759
  emissionId
@@ -2791,14 +2775,14 @@ namespace DxMessaging.Core
2791
2775
  {
2792
2776
  RunFastHandlers(
2793
2777
  ref source,
2794
- _fastBroadcastWithoutSourceHandlers,
2778
+ GetPriorityHandlers(TypedSlotIndex.BroadcastHandleWithoutContextFast),
2795
2779
  ref message,
2796
2780
  priority,
2797
2781
  emissionId
2798
2782
  );
2799
2783
  RunHandlers(
2800
2784
  ref source,
2801
- _broadcastWithoutSourceHandlers,
2785
+ GetPriorityHandlers(TypedSlotIndex.BroadcastHandleWithoutContext),
2802
2786
  ref message,
2803
2787
  priority,
2804
2788
  emissionId
@@ -2811,18 +2795,36 @@ namespace DxMessaging.Core
2811
2795
  /// <param name="message">Message to emit.</param>
2812
2796
  public void HandleGlobalUntargeted(ref IUntargetedMessage message, long emissionId)
2813
2797
  {
2814
- RunFastHandlers(_globalUntargetedFastHandlers, ref message, emissionId);
2815
- if (_globalUntargetedHandlers?.entries is not { Count: > 0 })
2798
+ HandlerActionCache<FastHandler<IUntargetedMessage>> fastCache = GetGlobalCache<
2799
+ FastHandler<IUntargetedMessage>
2800
+ >(TypedGlobalSlotIndex.UntargetedFast);
2801
+ RunFastHandlers(fastCache, ref message, emissionId);
2802
+ HandlerActionCache<Action<IUntargetedMessage>> cache = GetGlobalCache<
2803
+ Action<IUntargetedMessage>
2804
+ >(TypedGlobalSlotIndex.UntargetedDefault);
2805
+ // Live-count fast path. Cross-handler in-flight snapshot
2806
+ // semantics do not apply to the global accept-all path: the
2807
+ // bus dispatch loop calls PrefreezeGlobalUntargetedForEmission
2808
+ // lazily per-entry inside InvokeGlobalUntargetedEntry, after
2809
+ // earlier-priority handlers have already run. A sibling
2810
+ // MessageHandler that removes this handler's entry mid-emit
2811
+ // drains cache.entries before the lazy prefreeze can capture
2812
+ // a snapshot, so cache.cache rebuilds from the now-empty
2813
+ // entries. Bailing on cache.entries.Count == 0 is therefore
2814
+ // equivalent to bailing after GetOrAddNewHandlerStack would
2815
+ // return an empty list, and is documented behavior for the
2816
+ // global path.
2817
+ if (cache?.entries is not { Count: > 0 })
2816
2818
  {
2817
2819
  return;
2818
2820
  }
2819
2821
 
2820
2822
  List<Action<IUntargetedMessage>> handlers = GetOrAddNewHandlerStack(
2821
- _globalUntargetedHandlers,
2823
+ cache,
2822
2824
  emissionId
2823
2825
  );
2824
2826
  int handlersCount = handlers.Count;
2825
- for (int i = 0; i < handlersCount; ++i)
2827
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
2826
2828
  {
2827
2829
  handlers[i](message);
2828
2830
  }
@@ -2839,19 +2841,29 @@ namespace DxMessaging.Core
2839
2841
  long emissionId
2840
2842
  )
2841
2843
  {
2842
- RunFastHandlers(ref target, _globalTargetedFastHandlers, ref message, emissionId);
2843
-
2844
- if (_globalTargetedHandlers?.entries is not { Count: > 0 })
2844
+ HandlerActionCache<FastHandlerWithContext<ITargetedMessage>> fastCache =
2845
+ GetGlobalCache<FastHandlerWithContext<ITargetedMessage>>(
2846
+ TypedGlobalSlotIndex.TargetedFast
2847
+ );
2848
+ RunFastHandlers(ref target, fastCache, ref message, emissionId);
2849
+
2850
+ HandlerActionCache<Action<InstanceId, ITargetedMessage>> cache = GetGlobalCache<
2851
+ Action<InstanceId, ITargetedMessage>
2852
+ >(TypedGlobalSlotIndex.TargetedDefault);
2853
+ // Live-count fast path. See comment in HandleGlobalUntargeted
2854
+ // for why the global accept-all path bails on
2855
+ // cache.entries.Count == 0 rather than reading the snapshot.
2856
+ if (cache?.entries is not { Count: > 0 })
2845
2857
  {
2846
2858
  return;
2847
2859
  }
2848
2860
 
2849
2861
  List<Action<InstanceId, ITargetedMessage>> handlers = GetOrAddNewHandlerStack(
2850
- _globalTargetedHandlers,
2862
+ cache,
2851
2863
  emissionId
2852
2864
  );
2853
2865
  int handlersCount = handlers.Count;
2854
- for (int i = 0; i < handlersCount; ++i)
2866
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
2855
2867
  {
2856
2868
  handlers[i](target, message);
2857
2869
  }
@@ -2868,15 +2880,25 @@ namespace DxMessaging.Core
2868
2880
  long emissionId
2869
2881
  )
2870
2882
  {
2871
- RunFastHandlers(ref source, _globalBroadcastFastHandlers, ref message, emissionId);
2872
-
2873
- if (_globalBroadcastHandlers?.entries is not { Count: > 0 })
2883
+ HandlerActionCache<FastHandlerWithContext<IBroadcastMessage>> fastCache =
2884
+ GetGlobalCache<FastHandlerWithContext<IBroadcastMessage>>(
2885
+ TypedGlobalSlotIndex.BroadcastFast
2886
+ );
2887
+ RunFastHandlers(ref source, fastCache, ref message, emissionId);
2888
+
2889
+ HandlerActionCache<Action<InstanceId, IBroadcastMessage>> cache = GetGlobalCache<
2890
+ Action<InstanceId, IBroadcastMessage>
2891
+ >(TypedGlobalSlotIndex.BroadcastDefault);
2892
+ // Live-count fast path. See comment in HandleGlobalUntargeted
2893
+ // for why the global accept-all path bails on
2894
+ // cache.entries.Count == 0 rather than reading the snapshot.
2895
+ if (cache?.entries is not { Count: > 0 })
2874
2896
  {
2875
2897
  return;
2876
2898
  }
2877
2899
 
2878
2900
  List<Action<InstanceId, IBroadcastMessage>> handlers = GetOrAddNewHandlerStack(
2879
- _globalBroadcastHandlers,
2901
+ cache,
2880
2902
  emissionId
2881
2903
  );
2882
2904
  int handlersCount = handlers.Count;
@@ -2890,36 +2912,76 @@ namespace DxMessaging.Core
2890
2912
  case 2:
2891
2913
  {
2892
2914
  handlers[0](source, message);
2915
+ if (handlers.Count < 2)
2916
+ {
2917
+ return;
2918
+ }
2893
2919
  handlers[1](source, message);
2894
2920
  return;
2895
2921
  }
2896
2922
  case 3:
2897
2923
  {
2898
2924
  handlers[0](source, message);
2925
+ if (handlers.Count < 2)
2926
+ {
2927
+ return;
2928
+ }
2899
2929
  handlers[1](source, message);
2930
+ if (handlers.Count < 3)
2931
+ {
2932
+ return;
2933
+ }
2900
2934
  handlers[2](source, message);
2901
2935
  return;
2902
2936
  }
2903
2937
  case 4:
2904
2938
  {
2905
2939
  handlers[0](source, message);
2940
+ if (handlers.Count < 2)
2941
+ {
2942
+ return;
2943
+ }
2906
2944
  handlers[1](source, message);
2945
+ if (handlers.Count < 3)
2946
+ {
2947
+ return;
2948
+ }
2907
2949
  handlers[2](source, message);
2950
+ if (handlers.Count < 4)
2951
+ {
2952
+ return;
2953
+ }
2908
2954
  handlers[3](source, message);
2909
2955
  return;
2910
2956
  }
2911
2957
  case 5:
2912
2958
  {
2913
2959
  handlers[0](source, message);
2960
+ if (handlers.Count < 2)
2961
+ {
2962
+ return;
2963
+ }
2914
2964
  handlers[1](source, message);
2965
+ if (handlers.Count < 3)
2966
+ {
2967
+ return;
2968
+ }
2915
2969
  handlers[2](source, message);
2970
+ if (handlers.Count < 4)
2971
+ {
2972
+ return;
2973
+ }
2916
2974
  handlers[3](source, message);
2975
+ if (handlers.Count < 5)
2976
+ {
2977
+ return;
2978
+ }
2917
2979
  handlers[4](source, message);
2918
2980
  return;
2919
2981
  }
2920
2982
  }
2921
2983
 
2922
- for (int i = 0; i < handlersCount; ++i)
2984
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
2923
2985
  {
2924
2986
  handlers[i](source, message);
2925
2987
  }
@@ -2934,12 +2996,17 @@ namespace DxMessaging.Core
2934
2996
  public void HandleUntargetedPostProcessing(ref T message, int priority, long emissionId)
2935
2997
  {
2936
2998
  RunFastHandlers(
2937
- _untargetedPostProcessingFastHandlers,
2999
+ GetPriorityHandlers(TypedSlotIndex.UntargetedPostProcessFast),
3000
+ ref message,
3001
+ priority,
3002
+ emissionId
3003
+ );
3004
+ RunHandlers(
3005
+ GetPriorityHandlers(TypedSlotIndex.UntargetedPostProcessDefault),
2938
3006
  ref message,
2939
3007
  priority,
2940
3008
  emissionId
2941
3009
  );
2942
- RunHandlers(_untargetedPostProcessingHandlers, ref message, priority, emissionId);
2943
3010
  }
2944
3011
 
2945
3012
  /// <summary>
@@ -2958,14 +3025,14 @@ namespace DxMessaging.Core
2958
3025
  {
2959
3026
  RunFastHandlersWithContext(
2960
3027
  ref target,
2961
- _targetedPostProcessingFastHandlers,
3028
+ GetContextHandlers(TypedSlotIndex.TargetedPostProcessFast),
2962
3029
  ref message,
2963
3030
  priority,
2964
3031
  emissionId
2965
3032
  );
2966
3033
  RunHandlersWithContext(
2967
3034
  ref target,
2968
- _targetedPostProcessingHandlers,
3035
+ GetContextHandlers(TypedSlotIndex.TargetedPostProcessDefault),
2969
3036
  ref message,
2970
3037
  priority,
2971
3038
  emissionId
@@ -2988,14 +3055,14 @@ namespace DxMessaging.Core
2988
3055
  {
2989
3056
  RunFastHandlersWithContext(
2990
3057
  ref target,
2991
- _fastTargetedWithoutTargetingPostProcessingHandlers,
3058
+ GetPriorityHandlers(TypedSlotIndex.TargetedPostProcessWithoutContextFast),
2992
3059
  ref message,
2993
3060
  priority,
2994
3061
  emissionId
2995
3062
  );
2996
3063
  RunHandlers(
2997
3064
  ref target,
2998
- _targetedWithoutTargetingPostProcessingHandlers,
3065
+ GetPriorityHandlers(TypedSlotIndex.TargetedPostProcessWithoutContext),
2999
3066
  ref message,
3000
3067
  priority,
3001
3068
  emissionId
@@ -3018,14 +3085,14 @@ namespace DxMessaging.Core
3018
3085
  {
3019
3086
  RunFastHandlersWithContext(
3020
3087
  ref source,
3021
- _broadcastPostProcessingFastHandlers,
3088
+ GetContextHandlers(TypedSlotIndex.BroadcastPostProcessFast),
3022
3089
  ref message,
3023
3090
  priority,
3024
3091
  emissionId
3025
3092
  );
3026
3093
  RunHandlersWithContext(
3027
3094
  ref source,
3028
- _broadcastPostProcessingHandlers,
3095
+ GetContextHandlers(TypedSlotIndex.BroadcastPostProcessDefault),
3029
3096
  ref message,
3030
3097
  priority,
3031
3098
  emissionId
@@ -3048,14 +3115,14 @@ namespace DxMessaging.Core
3048
3115
  {
3049
3116
  RunFastHandlersWithContext(
3050
3117
  ref source,
3051
- _fastBroadcastWithoutSourcePostProcessingHandlers,
3118
+ GetPriorityHandlers(TypedSlotIndex.BroadcastPostProcessWithoutContextFast),
3052
3119
  ref message,
3053
3120
  priority,
3054
3121
  emissionId
3055
3122
  );
3056
3123
  RunHandlers(
3057
3124
  ref source,
3058
- _broadcastWithoutSourcePostProcessingHandlers,
3125
+ GetPriorityHandlers(TypedSlotIndex.BroadcastPostProcessWithoutContext),
3059
3126
  ref message,
3060
3127
  priority,
3061
3128
  emissionId
@@ -3076,17 +3143,22 @@ namespace DxMessaging.Core
3076
3143
  Action<T> handler,
3077
3144
  Action deregistration,
3078
3145
  int priority,
3079
- long emissionId
3146
+ IMessageBus messageBus
3080
3147
  )
3081
3148
  {
3082
- return AddHandler(
3149
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3150
+ // at registration time, so bus-side flat snapshot rebuilds
3151
+ // resolve default registrations without allocating closures.
3152
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
3153
+ return AddHandlerPreservingPriorityKey(
3083
3154
  target,
3084
- ref _targetedHandlers,
3155
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedHandleDefault),
3085
3156
  originalHandler,
3086
3157
  handler,
3087
3158
  deregistration,
3088
3159
  priority,
3089
- emissionId
3160
+ messageBus,
3161
+ flatInvoker
3090
3162
  );
3091
3163
  }
3092
3164
 
@@ -3104,17 +3176,17 @@ namespace DxMessaging.Core
3104
3176
  FastHandler<T> handler,
3105
3177
  Action deregistration,
3106
3178
  int priority,
3107
- long emissionId
3179
+ IMessageBus messageBus
3108
3180
  )
3109
3181
  {
3110
- return AddHandler(
3182
+ return AddHandlerPreservingPriorityKey(
3111
3183
  target,
3112
- ref _targetedFastHandlers,
3184
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedHandleFast),
3113
3185
  originalHandler,
3114
3186
  handler,
3115
3187
  deregistration,
3116
3188
  priority,
3117
- emissionId
3189
+ messageBus
3118
3190
  );
3119
3191
  }
3120
3192
 
@@ -3130,16 +3202,26 @@ namespace DxMessaging.Core
3130
3202
  Action<InstanceId, T> handler,
3131
3203
  Action deregistration,
3132
3204
  int priority,
3133
- long emissionId
3205
+ IMessageBus messageBus
3134
3206
  )
3135
3207
  {
3208
+ // Adapt the AUGMENTED handler to FastHandlerWithContext form
3209
+ // exactly once, at registration time, so bus-side flat snapshot
3210
+ // rebuilds resolve default registrations without allocating
3211
+ // closures.
3212
+ FastHandlerWithContext<T> flatInvoker = (ref InstanceId context, ref T message) =>
3213
+ handler(context, message);
3136
3214
  return AddHandlerPreservingPriorityKey(
3137
- ref _targetedWithoutTargetingHandlers,
3215
+ GetOrCreatePriorityHandlers(
3216
+ TypedSlotIndex.TargetedHandleWithoutContext,
3217
+ requiresContext: false
3218
+ ),
3138
3219
  originalHandler,
3139
3220
  handler,
3140
3221
  deregistration,
3141
3222
  priority,
3142
- emissionId
3223
+ messageBus,
3224
+ flatInvoker
3143
3225
  );
3144
3226
  }
3145
3227
 
@@ -3155,16 +3237,19 @@ namespace DxMessaging.Core
3155
3237
  FastHandlerWithContext<T> handler,
3156
3238
  Action deregistration,
3157
3239
  int priority,
3158
- long emissionId
3240
+ IMessageBus messageBus
3159
3241
  )
3160
3242
  {
3161
3243
  return AddHandlerPreservingPriorityKey(
3162
- ref _fastTargetedWithoutTargetingHandlers,
3244
+ GetOrCreatePriorityHandlers(
3245
+ TypedSlotIndex.TargetedHandleWithoutContextFast,
3246
+ requiresContext: false
3247
+ ),
3163
3248
  originalHandler,
3164
3249
  handler,
3165
3250
  deregistration,
3166
3251
  priority,
3167
- emissionId
3252
+ messageBus
3168
3253
  );
3169
3254
  }
3170
3255
 
@@ -3180,16 +3265,24 @@ namespace DxMessaging.Core
3180
3265
  Action<T> handler,
3181
3266
  Action deregistration,
3182
3267
  int priority,
3183
- long emissionId
3268
+ IMessageBus messageBus
3184
3269
  )
3185
3270
  {
3186
- return AddHandler(
3187
- ref _untargetedHandlers,
3271
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3272
+ // at registration time, so bus-side flat snapshot rebuilds
3273
+ // resolve default registrations without allocating closures.
3274
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
3275
+ return AddHandlerPreservingPriorityKey(
3276
+ GetOrCreatePriorityHandlers(
3277
+ TypedSlotIndex.UntargetedHandleDefault,
3278
+ requiresContext: false
3279
+ ),
3188
3280
  originalHandler,
3189
3281
  handler,
3190
3282
  deregistration,
3191
3283
  priority,
3192
- emissionId
3284
+ messageBus,
3285
+ flatInvoker
3193
3286
  );
3194
3287
  }
3195
3288
 
@@ -3205,16 +3298,19 @@ namespace DxMessaging.Core
3205
3298
  FastHandler<T> handler,
3206
3299
  Action deregistration,
3207
3300
  int priority,
3208
- long emissionId
3301
+ IMessageBus messageBus
3209
3302
  )
3210
3303
  {
3211
- return AddHandler(
3212
- ref _untargetedFastHandlers,
3304
+ return AddHandlerPreservingPriorityKey(
3305
+ GetOrCreatePriorityHandlers(
3306
+ TypedSlotIndex.UntargetedHandleFast,
3307
+ requiresContext: false
3308
+ ),
3213
3309
  originalHandler,
3214
3310
  handler,
3215
3311
  deregistration,
3216
3312
  priority,
3217
- emissionId
3313
+ messageBus
3218
3314
  );
3219
3315
  }
3220
3316
 
@@ -3232,17 +3328,22 @@ namespace DxMessaging.Core
3232
3328
  Action<T> handler,
3233
3329
  Action deregistration,
3234
3330
  int priority,
3235
- long emissionId
3331
+ IMessageBus messageBus
3236
3332
  )
3237
3333
  {
3238
- return AddHandler(
3334
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3335
+ // at registration time, so bus-side flat snapshot rebuilds
3336
+ // resolve default registrations without allocating closures.
3337
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
3338
+ return AddHandlerPreservingPriorityKey(
3239
3339
  source,
3240
- ref _broadcastHandlers,
3340
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastHandleDefault),
3241
3341
  originalHandler,
3242
3342
  handler,
3243
3343
  deregistration,
3244
3344
  priority,
3245
- emissionId
3345
+ messageBus,
3346
+ flatInvoker
3246
3347
  );
3247
3348
  }
3248
3349
 
@@ -3260,17 +3361,17 @@ namespace DxMessaging.Core
3260
3361
  FastHandler<T> handler,
3261
3362
  Action deregistration,
3262
3363
  int priority,
3263
- long emissionId
3364
+ IMessageBus messageBus
3264
3365
  )
3265
3366
  {
3266
- return AddHandler(
3367
+ return AddHandlerPreservingPriorityKey(
3267
3368
  source,
3268
- ref _broadcastFastHandlers,
3369
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastHandleFast),
3269
3370
  originalHandler,
3270
3371
  handler,
3271
3372
  deregistration,
3272
3373
  priority,
3273
- emissionId
3374
+ messageBus
3274
3375
  );
3275
3376
  }
3276
3377
 
@@ -3286,17 +3387,27 @@ namespace DxMessaging.Core
3286
3387
  Action<InstanceId, T> handler,
3287
3388
  Action deregistration,
3288
3389
  int priority,
3289
- long emissionId
3390
+ IMessageBus messageBus
3290
3391
  )
3291
3392
  {
3393
+ // Adapt the AUGMENTED handler to FastHandlerWithContext form
3394
+ // exactly once, at registration time, so bus-side flat snapshot
3395
+ // rebuilds resolve default registrations without allocating
3396
+ // closures.
3397
+ FastHandlerWithContext<T> flatInvoker = (ref InstanceId context, ref T message) =>
3398
+ handler(context, message);
3292
3399
  // Preserve the priority bucket during the current emission so frozen snapshots remain valid
3293
3400
  return AddHandlerPreservingPriorityKey(
3294
- ref _broadcastWithoutSourceHandlers,
3401
+ GetOrCreatePriorityHandlers(
3402
+ TypedSlotIndex.BroadcastHandleWithoutContext,
3403
+ requiresContext: false
3404
+ ),
3295
3405
  originalHandler,
3296
3406
  handler,
3297
3407
  deregistration,
3298
3408
  priority,
3299
- emissionId
3409
+ messageBus,
3410
+ flatInvoker
3300
3411
  );
3301
3412
  }
3302
3413
 
@@ -3312,17 +3423,20 @@ namespace DxMessaging.Core
3312
3423
  FastHandlerWithContext<T> handler,
3313
3424
  Action deregistration,
3314
3425
  int priority,
3315
- long emissionId
3426
+ IMessageBus messageBus
3316
3427
  )
3317
3428
  {
3318
3429
  // Preserve the priority bucket during the current emission so frozen snapshots remain valid
3319
3430
  return AddHandlerPreservingPriorityKey(
3320
- ref _fastBroadcastWithoutSourceHandlers,
3431
+ GetOrCreatePriorityHandlers(
3432
+ TypedSlotIndex.BroadcastHandleWithoutContextFast,
3433
+ requiresContext: false
3434
+ ),
3321
3435
  originalHandler,
3322
3436
  handler,
3323
3437
  deregistration,
3324
3438
  priority,
3325
- emissionId
3439
+ messageBus
3326
3440
  );
3327
3441
  }
3328
3442
 
@@ -3335,14 +3449,16 @@ namespace DxMessaging.Core
3335
3449
  public Action AddGlobalUntargetedHandler(
3336
3450
  Action<IUntargetedMessage> originalHandler,
3337
3451
  Action<IUntargetedMessage> handler,
3338
- Action deregistration
3452
+ Action deregistration,
3453
+ IMessageBus messageBus
3339
3454
  )
3340
3455
  {
3341
3456
  return AddHandler(
3342
- ref _globalUntargetedHandlers,
3457
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.UntargetedDefault),
3343
3458
  originalHandler,
3344
3459
  handler,
3345
- deregistration
3460
+ deregistration,
3461
+ messageBus
3346
3462
  );
3347
3463
  }
3348
3464
 
@@ -3355,14 +3471,16 @@ namespace DxMessaging.Core
3355
3471
  public Action AddGlobalUntargetedHandler(
3356
3472
  FastHandler<IUntargetedMessage> originalHandler,
3357
3473
  FastHandler<IUntargetedMessage> handler,
3358
- Action deregistration
3474
+ Action deregistration,
3475
+ IMessageBus messageBus
3359
3476
  )
3360
3477
  {
3361
3478
  return AddHandler(
3362
- ref _globalUntargetedFastHandlers,
3479
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.UntargetedFast),
3363
3480
  originalHandler,
3364
3481
  handler,
3365
- deregistration
3482
+ deregistration,
3483
+ messageBus
3366
3484
  );
3367
3485
  }
3368
3486
 
@@ -3375,14 +3493,16 @@ namespace DxMessaging.Core
3375
3493
  public Action AddGlobalTargetedHandler(
3376
3494
  Action<InstanceId, ITargetedMessage> originalHandler,
3377
3495
  Action<InstanceId, ITargetedMessage> handler,
3378
- Action deregistration
3496
+ Action deregistration,
3497
+ IMessageBus messageBus
3379
3498
  )
3380
3499
  {
3381
3500
  return AddHandler(
3382
- ref _globalTargetedHandlers,
3501
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.TargetedDefault),
3383
3502
  originalHandler,
3384
3503
  handler,
3385
- deregistration
3504
+ deregistration,
3505
+ messageBus
3386
3506
  );
3387
3507
  }
3388
3508
 
@@ -3395,14 +3515,16 @@ namespace DxMessaging.Core
3395
3515
  public Action AddGlobalTargetedHandler(
3396
3516
  FastHandlerWithContext<ITargetedMessage> originalHandler,
3397
3517
  FastHandlerWithContext<ITargetedMessage> handler,
3398
- Action deregistration
3518
+ Action deregistration,
3519
+ IMessageBus messageBus
3399
3520
  )
3400
3521
  {
3401
3522
  return AddHandler(
3402
- ref _globalTargetedFastHandlers,
3523
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.TargetedFast),
3403
3524
  originalHandler,
3404
3525
  handler,
3405
- deregistration
3526
+ deregistration,
3527
+ messageBus
3406
3528
  );
3407
3529
  }
3408
3530
 
@@ -3415,14 +3537,16 @@ namespace DxMessaging.Core
3415
3537
  public Action AddGlobalBroadcastHandler(
3416
3538
  Action<InstanceId, IBroadcastMessage> originalHandler,
3417
3539
  Action<InstanceId, IBroadcastMessage> handler,
3418
- Action deregistration
3540
+ Action deregistration,
3541
+ IMessageBus messageBus
3419
3542
  )
3420
3543
  {
3421
3544
  return AddHandler(
3422
- ref _globalBroadcastHandlers,
3545
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.BroadcastDefault),
3423
3546
  originalHandler,
3424
3547
  handler,
3425
- deregistration
3548
+ deregistration,
3549
+ messageBus
3426
3550
  );
3427
3551
  }
3428
3552
 
@@ -3435,14 +3559,16 @@ namespace DxMessaging.Core
3435
3559
  public Action AddGlobalBroadcastHandler(
3436
3560
  FastHandlerWithContext<IBroadcastMessage> originalHandler,
3437
3561
  FastHandlerWithContext<IBroadcastMessage> handler,
3438
- Action deregistration
3562
+ Action deregistration,
3563
+ IMessageBus messageBus
3439
3564
  )
3440
3565
  {
3441
3566
  return AddHandler(
3442
- ref _globalBroadcastFastHandlers,
3567
+ GetOrCreateGlobalSlot(TypedGlobalSlotIndex.BroadcastFast),
3443
3568
  originalHandler,
3444
3569
  handler,
3445
- deregistration
3570
+ deregistration,
3571
+ messageBus
3446
3572
  );
3447
3573
  }
3448
3574
 
@@ -3458,16 +3584,24 @@ namespace DxMessaging.Core
3458
3584
  Action<T> handler,
3459
3585
  Action deregistration,
3460
3586
  int priority,
3461
- long emissionId
3587
+ IMessageBus messageBus
3462
3588
  )
3463
3589
  {
3590
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3591
+ // at registration time, so bus-side flat snapshot rebuilds
3592
+ // resolve default registrations without allocating closures.
3593
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
3464
3594
  return AddHandlerPreservingPriorityKey(
3465
- ref _untargetedPostProcessingHandlers,
3595
+ GetOrCreatePriorityHandlers(
3596
+ TypedSlotIndex.UntargetedPostProcessDefault,
3597
+ requiresContext: false
3598
+ ),
3466
3599
  originalHandler,
3467
3600
  handler,
3468
3601
  deregistration,
3469
3602
  priority,
3470
- emissionId
3603
+ messageBus,
3604
+ flatInvoker
3471
3605
  );
3472
3606
  }
3473
3607
 
@@ -3483,16 +3617,19 @@ namespace DxMessaging.Core
3483
3617
  FastHandler<T> handler,
3484
3618
  Action deregistration,
3485
3619
  int priority,
3486
- long emissionId
3620
+ IMessageBus messageBus
3487
3621
  )
3488
3622
  {
3489
3623
  return AddHandlerPreservingPriorityKey(
3490
- ref _untargetedPostProcessingFastHandlers,
3624
+ GetOrCreatePriorityHandlers(
3625
+ TypedSlotIndex.UntargetedPostProcessFast,
3626
+ requiresContext: false
3627
+ ),
3491
3628
  originalHandler,
3492
3629
  handler,
3493
3630
  deregistration,
3494
3631
  priority,
3495
- emissionId
3632
+ messageBus
3496
3633
  );
3497
3634
  }
3498
3635
 
@@ -3510,17 +3647,22 @@ namespace DxMessaging.Core
3510
3647
  Action<T> handler,
3511
3648
  Action deregistration,
3512
3649
  int priority,
3513
- long emissionId
3650
+ IMessageBus messageBus
3514
3651
  )
3515
3652
  {
3653
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3654
+ // at registration time, so bus-side flat snapshot rebuilds
3655
+ // resolve default registrations without allocating closures.
3656
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
3516
3657
  return AddHandlerPreservingPriorityKey(
3517
3658
  target,
3518
- ref _targetedPostProcessingHandlers,
3659
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedPostProcessDefault),
3519
3660
  originalHandler,
3520
3661
  handler,
3521
3662
  deregistration,
3522
3663
  priority,
3523
- emissionId
3664
+ messageBus,
3665
+ flatInvoker
3524
3666
  );
3525
3667
  }
3526
3668
 
@@ -3538,17 +3680,17 @@ namespace DxMessaging.Core
3538
3680
  FastHandler<T> handler,
3539
3681
  Action deregistration,
3540
3682
  int priority,
3541
- long emissionId
3683
+ IMessageBus messageBus
3542
3684
  )
3543
3685
  {
3544
3686
  return AddHandlerPreservingPriorityKey(
3545
3687
  target,
3546
- ref _targetedPostProcessingFastHandlers,
3688
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedPostProcessFast),
3547
3689
  originalHandler,
3548
3690
  handler,
3549
3691
  deregistration,
3550
3692
  priority,
3551
- emissionId
3693
+ messageBus
3552
3694
  );
3553
3695
  }
3554
3696
 
@@ -3564,16 +3706,26 @@ namespace DxMessaging.Core
3564
3706
  Action<InstanceId, T> handler,
3565
3707
  Action deregistration,
3566
3708
  int priority,
3567
- long emissionId
3709
+ IMessageBus messageBus
3568
3710
  )
3569
3711
  {
3712
+ // Adapt the AUGMENTED handler to FastHandlerWithContext form
3713
+ // exactly once, at registration time, so bus-side flat snapshot
3714
+ // rebuilds resolve default registrations without allocating
3715
+ // closures.
3716
+ FastHandlerWithContext<T> flatInvoker = (ref InstanceId context, ref T message) =>
3717
+ handler(context, message);
3570
3718
  return AddHandlerPreservingPriorityKey(
3571
- ref _targetedWithoutTargetingPostProcessingHandlers,
3719
+ GetOrCreatePriorityHandlers(
3720
+ TypedSlotIndex.TargetedPostProcessWithoutContext,
3721
+ requiresContext: false
3722
+ ),
3572
3723
  originalHandler,
3573
3724
  handler,
3574
3725
  deregistration,
3575
3726
  priority,
3576
- emissionId
3727
+ messageBus,
3728
+ flatInvoker
3577
3729
  );
3578
3730
  }
3579
3731
 
@@ -3589,16 +3741,19 @@ namespace DxMessaging.Core
3589
3741
  FastHandlerWithContext<T> handler,
3590
3742
  Action deregistration,
3591
3743
  int priority,
3592
- long emissionId
3744
+ IMessageBus messageBus
3593
3745
  )
3594
3746
  {
3595
3747
  return AddHandlerPreservingPriorityKey(
3596
- ref _fastTargetedWithoutTargetingPostProcessingHandlers,
3748
+ GetOrCreatePriorityHandlers(
3749
+ TypedSlotIndex.TargetedPostProcessWithoutContextFast,
3750
+ requiresContext: false
3751
+ ),
3597
3752
  originalHandler,
3598
3753
  handler,
3599
3754
  deregistration,
3600
3755
  priority,
3601
- emissionId
3756
+ messageBus
3602
3757
  );
3603
3758
  }
3604
3759
 
@@ -3616,17 +3771,22 @@ namespace DxMessaging.Core
3616
3771
  Action<T> handler,
3617
3772
  Action deregistration,
3618
3773
  int priority,
3619
- long emissionId
3774
+ IMessageBus messageBus
3620
3775
  )
3621
3776
  {
3777
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3778
+ // at registration time, so bus-side flat snapshot rebuilds
3779
+ // resolve default registrations without allocating closures.
3780
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
3622
3781
  return AddHandlerPreservingPriorityKey(
3623
3782
  source,
3624
- ref _broadcastPostProcessingHandlers,
3783
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastPostProcessDefault),
3625
3784
  originalHandler,
3626
3785
  handler,
3627
3786
  deregistration,
3628
3787
  priority,
3629
- emissionId
3788
+ messageBus,
3789
+ flatInvoker
3630
3790
  );
3631
3791
  }
3632
3792
 
@@ -3644,17 +3804,17 @@ namespace DxMessaging.Core
3644
3804
  FastHandler<T> handler,
3645
3805
  Action deregistration,
3646
3806
  int priority,
3647
- long emissionId
3807
+ IMessageBus messageBus
3648
3808
  )
3649
3809
  {
3650
3810
  return AddHandlerPreservingPriorityKey(
3651
3811
  source,
3652
- ref _broadcastPostProcessingFastHandlers,
3812
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastPostProcessFast),
3653
3813
  originalHandler,
3654
3814
  handler,
3655
3815
  deregistration,
3656
3816
  priority,
3657
- emissionId
3817
+ messageBus
3658
3818
  );
3659
3819
  }
3660
3820
 
@@ -3670,16 +3830,26 @@ namespace DxMessaging.Core
3670
3830
  Action<InstanceId, T> handler,
3671
3831
  Action deregistration,
3672
3832
  int priority,
3673
- long emissionId
3833
+ IMessageBus messageBus
3674
3834
  )
3675
3835
  {
3836
+ // Adapt the AUGMENTED handler to FastHandlerWithContext form
3837
+ // exactly once, at registration time, so bus-side flat snapshot
3838
+ // rebuilds resolve default registrations without allocating
3839
+ // closures.
3840
+ FastHandlerWithContext<T> flatInvoker = (ref InstanceId context, ref T message) =>
3841
+ handler(context, message);
3676
3842
  return AddHandlerPreservingPriorityKey(
3677
- ref _broadcastWithoutSourcePostProcessingHandlers,
3843
+ GetOrCreatePriorityHandlers(
3844
+ TypedSlotIndex.BroadcastPostProcessWithoutContext,
3845
+ requiresContext: false
3846
+ ),
3678
3847
  originalHandler,
3679
3848
  handler,
3680
3849
  deregistration,
3681
3850
  priority,
3682
- emissionId
3851
+ messageBus,
3852
+ flatInvoker
3683
3853
  );
3684
3854
  }
3685
3855
 
@@ -3695,20 +3865,204 @@ namespace DxMessaging.Core
3695
3865
  FastHandlerWithContext<T> handler,
3696
3866
  Action deregistration,
3697
3867
  int priority,
3698
- long emissionId
3868
+ IMessageBus messageBus
3699
3869
  )
3700
3870
  {
3701
3871
  return AddHandlerPreservingPriorityKey(
3702
- ref _fastBroadcastWithoutSourcePostProcessingHandlers,
3872
+ GetOrCreatePriorityHandlers(
3873
+ TypedSlotIndex.BroadcastPostProcessWithoutContextFast,
3874
+ requiresContext: false
3875
+ ),
3703
3876
  originalHandler,
3704
3877
  handler,
3705
3878
  deregistration,
3706
3879
  priority,
3707
- emissionId
3880
+ messageBus
3708
3881
  );
3709
3882
  }
3710
3883
 
3711
- // Context-aware variant that preserves the priority key mapping on deregistration for the current emission.
3884
+ // Context-aware variant that preserves the priority and context key
3885
+ // mappings on deregistration so frozen dispatch snapshots remain valid
3886
+ // for any in-flight emission. Trade-off: empty HandlerActionCache
3887
+ // entries (and their enclosing per-priority Dictionary) are not
3888
+ // reclaimed until either (a) a future registration at the same
3889
+ // (context, priority) pair reuses the cache, or (b) the owning
3890
+ // MessageHandler is destroyed. For typical Unity gameplay (a small
3891
+ // fixed set of priorities and a bounded set of long-lived target /
3892
+ // source InstanceIds) the residual footprint is on the order of
3893
+ // hundreds of bytes per MessageHandler. Code that interacts with
3894
+ // many transient InstanceIds (e.g. a global service that registers
3895
+ // handlers per ephemeral GameObject) should prefer recycling
3896
+ // MessageHandlers or routing through AddSourcedBroadcastWithoutSourceHandler /
3897
+ // AddTargetedWithoutTargetingHandler to avoid the per-(context,priority)
3898
+ // outer-dictionary growth.
3899
+ // `flatInvoker` carries the pre-resolved flat-dispatch invoker for
3900
+ // default-shape registrations the bus-side flat snapshot consumes
3901
+ // (FastHandler adapter wrapping the augmented handler); see
3902
+ // HandlerActionCache.Entry.flatInvoker.
3903
+ private Action AddHandlerPreservingPriorityKey<TU>(
3904
+ InstanceId context,
3905
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext,
3906
+ TU originalHandler,
3907
+ TU augmentedHandler,
3908
+ Action deregistration,
3909
+ int priority,
3910
+ IMessageBus messageBus,
3911
+ object flatInvoker = null
3912
+ )
3913
+ {
3914
+ if (
3915
+ !handlersByContext.TryGetValue(
3916
+ context,
3917
+ out Dictionary<int, IHandlerActionCache> sortedHandlers
3918
+ )
3919
+ )
3920
+ {
3921
+ sortedHandlers = DxPools.TypedHandlerPriorityDicts.Rent();
3922
+ handlersByContext[context] = sortedHandlers;
3923
+ }
3924
+
3925
+ if (
3926
+ !sortedHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
3927
+ || erasedCache is not HandlerActionCache<TU> cache
3928
+ )
3929
+ {
3930
+ cache = new HandlerActionCache<TU>();
3931
+ sortedHandlers[priority] = cache;
3932
+ }
3933
+
3934
+ if (
3935
+ !cache.entries.TryGetValue(
3936
+ originalHandler,
3937
+ out HandlerActionCache<TU>.Entry entry
3938
+ )
3939
+ )
3940
+ {
3941
+ entry = new HandlerActionCache<TU>.Entry(augmentedHandler, 0);
3942
+ }
3943
+
3944
+ bool firstRegistration = entry.count == 0;
3945
+ entry = firstRegistration
3946
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1, flatInvoker)
3947
+ : new HandlerActionCache<TU>.Entry(
3948
+ entry.handler,
3949
+ entry.count + 1,
3950
+ entry.flatInvoker
3951
+ );
3952
+
3953
+ cache.entries[originalHandler] = entry;
3954
+ if (firstRegistration)
3955
+ {
3956
+ cache.insertionOrder.Add(originalHandler);
3957
+ }
3958
+ cache.version++;
3959
+ TypedSlot<T> slot = FindContextSlot(handlersByContext);
3960
+ if (slot != null)
3961
+ {
3962
+ slot.lastTouchTicks =
3963
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
3964
+ messageBus
3965
+ );
3966
+ }
3967
+ if (firstRegistration && slot != null)
3968
+ {
3969
+ slot.liveCount++;
3970
+ }
3971
+
3972
+ Dictionary<
3973
+ InstanceId,
3974
+ Dictionary<int, IHandlerActionCache>
3975
+ > localHandlersByContext = handlersByContext;
3976
+ TypedSlot<T> localSlot = slot;
3977
+ long localSlotVersion = slot?.version ?? 0;
3978
+ long localResetGeneration =
3979
+ global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
3980
+
3981
+ return () =>
3982
+ {
3983
+ if (
3984
+ !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
3985
+ messageBus,
3986
+ localResetGeneration
3987
+ )
3988
+ )
3989
+ {
3990
+ return;
3991
+ }
3992
+
3993
+ if (localSlot != null && localSlot.version != localSlotVersion)
3994
+ {
3995
+ return;
3996
+ }
3997
+
3998
+ if (!localHandlersByContext.TryGetValue(context, out sortedHandlers))
3999
+ {
4000
+ return;
4001
+ }
4002
+
4003
+ if (
4004
+ !sortedHandlers.TryGetValue(
4005
+ priority,
4006
+ out IHandlerActionCache localErasedCache
4007
+ ) || localErasedCache is not HandlerActionCache<TU> localCache
4008
+ )
4009
+ {
4010
+ return;
4011
+ }
4012
+
4013
+ if (
4014
+ !localCache.entries.TryGetValue(
4015
+ originalHandler,
4016
+ out HandlerActionCache<TU>.Entry localEntry
4017
+ )
4018
+ )
4019
+ {
4020
+ return;
4021
+ }
4022
+
4023
+ localCache.version++;
4024
+
4025
+ deregistration?.Invoke();
4026
+ if (localSlot != null)
4027
+ {
4028
+ localSlot.lastTouchTicks =
4029
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
4030
+ messageBus
4031
+ );
4032
+ }
4033
+
4034
+ if (localEntry.count <= 1)
4035
+ {
4036
+ _ = localCache.entries.Remove(originalHandler);
4037
+ // List.Remove is O(n) over the same-priority bucket.
4038
+ // Accepted tradeoff (here and at every sibling
4039
+ // deregistration site): buckets are small in practice,
4040
+ // removal is a cold churn path, and the list keeps
4041
+ // steady-state dispatch allocation-free while
4042
+ // preserving first-registration order, unlike
4043
+ // Dictionary enumeration whose freed slots are reused
4044
+ // LIFO.
4045
+ _ = localCache.insertionOrder.Remove(originalHandler);
4046
+ localCache.version++;
4047
+ if (localSlot != null)
4048
+ {
4049
+ localSlot.liveCount--;
4050
+ }
4051
+ // Deliberately keep the priority and context mappings to preserve
4052
+ // frozen snapshots for the current emission.
4053
+ return;
4054
+ }
4055
+
4056
+ localEntry = new HandlerActionCache<TU>.Entry(
4057
+ localEntry.handler,
4058
+ localEntry.count - 1,
4059
+ localEntry.flatInvoker
4060
+ );
4061
+
4062
+ localCache.entries[originalHandler] = localEntry;
4063
+ };
4064
+ }
4065
+
3712
4066
  private static Action AddHandlerPreservingPriorityKey<TU>(
3713
4067
  InstanceId context,
3714
4068
  ref Dictionary<
@@ -3719,7 +4073,7 @@ namespace DxMessaging.Core
3719
4073
  TU augmentedHandler,
3720
4074
  Action deregistration,
3721
4075
  int priority,
3722
- long emissionId
4076
+ IMessageBus messageBus
3723
4077
  )
3724
4078
  {
3725
4079
  handlersByContext ??=
@@ -3758,6 +4112,10 @@ namespace DxMessaging.Core
3758
4112
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
3759
4113
 
3760
4114
  cache.entries[originalHandler] = entry;
4115
+ if (firstRegistration)
4116
+ {
4117
+ cache.insertionOrder.Add(originalHandler);
4118
+ }
3761
4119
  cache.version++;
3762
4120
 
3763
4121
  Dictionary<
@@ -3796,6 +4154,7 @@ namespace DxMessaging.Core
3796
4154
  if (localEntry.count <= 1)
3797
4155
  {
3798
4156
  _ = localCache.entries.Remove(originalHandler);
4157
+ _ = localCache.insertionOrder.Remove(originalHandler);
3799
4158
  localCache.version++;
3800
4159
  // Deliberately keep the priority and context mappings to preserve
3801
4160
  // frozen snapshots for the current emission.
@@ -3807,8 +4166,43 @@ namespace DxMessaging.Core
3807
4166
  localEntry.count - 1
3808
4167
  );
3809
4168
 
3810
- localCache.entries[originalHandler] = localEntry;
3811
- };
4169
+ localCache.entries[originalHandler] = localEntry;
4170
+ };
4171
+ }
4172
+
4173
+ private static void RunFastHandlersWithContext<TMessage>(
4174
+ ref InstanceId context,
4175
+ Dictionary<int, IHandlerActionCache> fastHandlers,
4176
+ ref TMessage message,
4177
+ int priority,
4178
+ long emissionId
4179
+ )
4180
+ where TMessage : IMessage
4181
+ {
4182
+ RunFastHandlers(ref context, fastHandlers, ref message, priority, emissionId);
4183
+ }
4184
+
4185
+ private static void RunFastHandlersWithContext<TMessage>(
4186
+ ref InstanceId context,
4187
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> fastHandlersByContext,
4188
+ ref TMessage message,
4189
+ int priority,
4190
+ long emissionId
4191
+ )
4192
+ where TMessage : IMessage
4193
+ {
4194
+ if (
4195
+ fastHandlersByContext is not { Count: > 0 }
4196
+ || !fastHandlersByContext.TryGetValue(
4197
+ context,
4198
+ out Dictionary<int, IHandlerActionCache> cache
4199
+ )
4200
+ )
4201
+ {
4202
+ return;
4203
+ }
4204
+
4205
+ RunFastHandlers(cache, ref message, priority, emissionId);
3812
4206
  }
3813
4207
 
3814
4208
  private static void RunFastHandlersWithContext<TMessage>(
@@ -3863,6 +4257,115 @@ namespace DxMessaging.Core
3863
4257
  RunFastHandlers(cache, ref message, priority, emissionId);
3864
4258
  }
3865
4259
 
4260
+ private static void RunFastHandlers<TMessage>(
4261
+ Dictionary<int, IHandlerActionCache> fastHandlers,
4262
+ ref TMessage message,
4263
+ int priority,
4264
+ long emissionId
4265
+ )
4266
+ where TMessage : IMessage
4267
+ {
4268
+ if (fastHandlers is not { Count: > 0 })
4269
+ {
4270
+ return;
4271
+ }
4272
+
4273
+ if (
4274
+ !fastHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
4275
+ || erasedCache is not HandlerActionCache<FastHandler<T>> cache
4276
+ )
4277
+ {
4278
+ return;
4279
+ }
4280
+
4281
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
4282
+ List<FastHandler<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
4283
+ int handlersCount = handlers.Count;
4284
+ switch (handlersCount)
4285
+ {
4286
+ case 1:
4287
+ {
4288
+ handlers[0](ref typedMessage);
4289
+ return;
4290
+ }
4291
+ case 2:
4292
+ {
4293
+ handlers[0](ref typedMessage);
4294
+ if (handlers.Count < 2)
4295
+ {
4296
+ return;
4297
+ }
4298
+ handlers[1](ref typedMessage);
4299
+ return;
4300
+ }
4301
+ case 3:
4302
+ {
4303
+ handlers[0](ref typedMessage);
4304
+ if (handlers.Count < 2)
4305
+ {
4306
+ return;
4307
+ }
4308
+ handlers[1](ref typedMessage);
4309
+ if (handlers.Count < 3)
4310
+ {
4311
+ return;
4312
+ }
4313
+ handlers[2](ref typedMessage);
4314
+ return;
4315
+ }
4316
+ case 4:
4317
+ {
4318
+ handlers[0](ref typedMessage);
4319
+ if (handlers.Count < 2)
4320
+ {
4321
+ return;
4322
+ }
4323
+ handlers[1](ref typedMessage);
4324
+ if (handlers.Count < 3)
4325
+ {
4326
+ return;
4327
+ }
4328
+ handlers[2](ref typedMessage);
4329
+ if (handlers.Count < 4)
4330
+ {
4331
+ return;
4332
+ }
4333
+ handlers[3](ref typedMessage);
4334
+ return;
4335
+ }
4336
+ case 5:
4337
+ {
4338
+ handlers[0](ref typedMessage);
4339
+ if (handlers.Count < 2)
4340
+ {
4341
+ return;
4342
+ }
4343
+ handlers[1](ref typedMessage);
4344
+ if (handlers.Count < 3)
4345
+ {
4346
+ return;
4347
+ }
4348
+ handlers[2](ref typedMessage);
4349
+ if (handlers.Count < 4)
4350
+ {
4351
+ return;
4352
+ }
4353
+ handlers[3](ref typedMessage);
4354
+ if (handlers.Count < 5)
4355
+ {
4356
+ return;
4357
+ }
4358
+ handlers[4](ref typedMessage);
4359
+ return;
4360
+ }
4361
+ }
4362
+
4363
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
4364
+ {
4365
+ handlers[i](ref typedMessage);
4366
+ }
4367
+ }
4368
+
3866
4369
  private static void RunFastHandlers<TMessage>(
3867
4370
  Dictionary<int, HandlerActionCache<FastHandler<T>>> fastHandlers,
3868
4371
  ref TMessage message,
@@ -3886,7 +4389,7 @@ namespace DxMessaging.Core
3886
4389
  return;
3887
4390
  }
3888
4391
 
3889
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
4392
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
3890
4393
  List<FastHandler<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
3891
4394
  int handlersCount = handlers.Count;
3892
4395
  switch (handlersCount)
@@ -3899,36 +4402,76 @@ namespace DxMessaging.Core
3899
4402
  case 2:
3900
4403
  {
3901
4404
  handlers[0](ref typedMessage);
4405
+ if (handlers.Count < 2)
4406
+ {
4407
+ return;
4408
+ }
3902
4409
  handlers[1](ref typedMessage);
3903
4410
  return;
3904
4411
  }
3905
4412
  case 3:
3906
4413
  {
3907
4414
  handlers[0](ref typedMessage);
4415
+ if (handlers.Count < 2)
4416
+ {
4417
+ return;
4418
+ }
3908
4419
  handlers[1](ref typedMessage);
4420
+ if (handlers.Count < 3)
4421
+ {
4422
+ return;
4423
+ }
3909
4424
  handlers[2](ref typedMessage);
3910
4425
  return;
3911
4426
  }
3912
4427
  case 4:
3913
4428
  {
3914
4429
  handlers[0](ref typedMessage);
4430
+ if (handlers.Count < 2)
4431
+ {
4432
+ return;
4433
+ }
3915
4434
  handlers[1](ref typedMessage);
4435
+ if (handlers.Count < 3)
4436
+ {
4437
+ return;
4438
+ }
3916
4439
  handlers[2](ref typedMessage);
4440
+ if (handlers.Count < 4)
4441
+ {
4442
+ return;
4443
+ }
3917
4444
  handlers[3](ref typedMessage);
3918
4445
  return;
3919
4446
  }
3920
4447
  case 5:
3921
4448
  {
3922
4449
  handlers[0](ref typedMessage);
4450
+ if (handlers.Count < 2)
4451
+ {
4452
+ return;
4453
+ }
3923
4454
  handlers[1](ref typedMessage);
4455
+ if (handlers.Count < 3)
4456
+ {
4457
+ return;
4458
+ }
3924
4459
  handlers[2](ref typedMessage);
4460
+ if (handlers.Count < 4)
4461
+ {
4462
+ return;
4463
+ }
3925
4464
  handlers[3](ref typedMessage);
4465
+ if (handlers.Count < 5)
4466
+ {
4467
+ return;
4468
+ }
3926
4469
  handlers[4](ref typedMessage);
3927
4470
  return;
3928
4471
  }
3929
4472
  }
3930
4473
 
3931
- for (int i = 0; i < handlersCount; ++i)
4474
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
3932
4475
  {
3933
4476
  handlers[i](ref typedMessage);
3934
4477
  }
@@ -3942,14 +4485,33 @@ namespace DxMessaging.Core
3942
4485
  where TMessage : IMessage
3943
4486
  where TU : IMessage
3944
4487
  {
3945
- if (cache?.entries is not { Count: > 0 })
4488
+ // Snapshot semantics: do not bail on the live entries dictionary
4489
+ // count. A mid-emit removal can drain entries while the pinned
4490
+ // emission snapshot in cache.cache still holds the handlers we
4491
+ // must invoke. Read the snapshot first and bail only if the
4492
+ // snapshot itself is empty.
4493
+ //
4494
+ // Perf note: GetOrAddNewHandlerStack is now invoked on every
4495
+ // call (including for empty caches that the previous fast-path
4496
+ // would have skipped). The cost is one dictionary
4497
+ // emission-id/version compare and -- only when the per-emission
4498
+ // snapshot has not been pinned yet -- a single pass over
4499
+ // cache.entries to materialise an empty list. The win is
4500
+ // correctness across cross-handler mid-emit removals where the
4501
+ // pinned snapshot in cache.cache still holds handlers the live
4502
+ // entries dictionary no longer reaches.
4503
+ if (cache == null)
3946
4504
  {
3947
4505
  return;
3948
4506
  }
3949
4507
 
3950
- ref TU typedMessage = ref Unsafe.As<TMessage, TU>(ref message);
4508
+ ref TU typedMessage = ref DxUnsafe.As<TMessage, TU>(ref message);
3951
4509
  List<FastHandler<TU>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
3952
4510
  int handlersCount = handlers.Count;
4511
+ if (handlersCount == 0)
4512
+ {
4513
+ return;
4514
+ }
3953
4515
  switch (handlersCount)
3954
4516
  {
3955
4517
  case 1:
@@ -3960,56 +4522,270 @@ namespace DxMessaging.Core
3960
4522
  case 2:
3961
4523
  {
3962
4524
  handlers[0](ref typedMessage);
4525
+ if (handlers.Count < 2)
4526
+ {
4527
+ return;
4528
+ }
3963
4529
  handlers[1](ref typedMessage);
3964
4530
  return;
3965
4531
  }
3966
4532
  case 3:
3967
4533
  {
3968
4534
  handlers[0](ref typedMessage);
4535
+ if (handlers.Count < 2)
4536
+ {
4537
+ return;
4538
+ }
3969
4539
  handlers[1](ref typedMessage);
4540
+ if (handlers.Count < 3)
4541
+ {
4542
+ return;
4543
+ }
3970
4544
  handlers[2](ref typedMessage);
3971
4545
  return;
3972
4546
  }
3973
4547
  case 4:
3974
4548
  {
3975
4549
  handlers[0](ref typedMessage);
4550
+ if (handlers.Count < 2)
4551
+ {
4552
+ return;
4553
+ }
3976
4554
  handlers[1](ref typedMessage);
4555
+ if (handlers.Count < 3)
4556
+ {
4557
+ return;
4558
+ }
3977
4559
  handlers[2](ref typedMessage);
4560
+ if (handlers.Count < 4)
4561
+ {
4562
+ return;
4563
+ }
3978
4564
  handlers[3](ref typedMessage);
3979
4565
  return;
3980
4566
  }
3981
4567
  case 5:
3982
4568
  {
3983
4569
  handlers[0](ref typedMessage);
4570
+ if (handlers.Count < 2)
4571
+ {
4572
+ return;
4573
+ }
3984
4574
  handlers[1](ref typedMessage);
4575
+ if (handlers.Count < 3)
4576
+ {
4577
+ return;
4578
+ }
3985
4579
  handlers[2](ref typedMessage);
4580
+ if (handlers.Count < 4)
4581
+ {
4582
+ return;
4583
+ }
3986
4584
  handlers[3](ref typedMessage);
4585
+ if (handlers.Count < 5)
4586
+ {
4587
+ return;
4588
+ }
3987
4589
  handlers[4](ref typedMessage);
3988
4590
  return;
3989
4591
  }
3990
4592
  }
3991
4593
 
3992
- for (int i = 0; i < handlersCount; ++i)
4594
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
4595
+ {
4596
+ handlers[i](ref typedMessage);
4597
+ }
4598
+ }
4599
+
4600
+ private static void RunFastHandlers<TMessage, TU>(
4601
+ ref InstanceId context,
4602
+ HandlerActionCache<FastHandlerWithContext<TU>> cache,
4603
+ ref TMessage message,
4604
+ long emissionId
4605
+ )
4606
+ where TMessage : IMessage
4607
+ where TU : IMessage
4608
+ {
4609
+ // Snapshot semantics: see comment on the FastHandler<TU> overload.
4610
+ // The pinned emission snapshot may still hold handlers even when
4611
+ // the live entries dictionary has been drained mid-emit.
4612
+ if (cache == null)
4613
+ {
4614
+ return;
4615
+ }
4616
+
4617
+ ref TU typedMessage = ref DxUnsafe.As<TMessage, TU>(ref message);
4618
+ List<FastHandlerWithContext<TU>> handlers = GetOrAddNewHandlerStack(
4619
+ cache,
4620
+ emissionId
4621
+ );
4622
+ int handlersCount = handlers.Count;
4623
+ if (handlersCount == 0)
4624
+ {
4625
+ return;
4626
+ }
4627
+ switch (handlersCount)
4628
+ {
4629
+ case 1:
4630
+ {
4631
+ handlers[0](ref context, ref typedMessage);
4632
+ return;
4633
+ }
4634
+ case 2:
4635
+ {
4636
+ handlers[0](ref context, ref typedMessage);
4637
+ if (handlers.Count < 2)
4638
+ {
4639
+ return;
4640
+ }
4641
+ handlers[1](ref context, ref typedMessage);
4642
+ return;
4643
+ }
4644
+ case 3:
4645
+ {
4646
+ handlers[0](ref context, ref typedMessage);
4647
+ if (handlers.Count < 2)
4648
+ {
4649
+ return;
4650
+ }
4651
+ handlers[1](ref context, ref typedMessage);
4652
+ if (handlers.Count < 3)
4653
+ {
4654
+ return;
4655
+ }
4656
+ handlers[2](ref context, ref typedMessage);
4657
+ return;
4658
+ }
4659
+ case 4:
4660
+ {
4661
+ handlers[0](ref context, ref typedMessage);
4662
+ if (handlers.Count < 2)
4663
+ {
4664
+ return;
4665
+ }
4666
+ handlers[1](ref context, ref typedMessage);
4667
+ if (handlers.Count < 3)
4668
+ {
4669
+ return;
4670
+ }
4671
+ handlers[2](ref context, ref typedMessage);
4672
+ if (handlers.Count < 4)
4673
+ {
4674
+ return;
4675
+ }
4676
+ handlers[3](ref context, ref typedMessage);
4677
+ return;
4678
+ }
4679
+ case 5:
4680
+ {
4681
+ handlers[0](ref context, ref typedMessage);
4682
+ if (handlers.Count < 2)
4683
+ {
4684
+ return;
4685
+ }
4686
+ handlers[1](ref context, ref typedMessage);
4687
+ if (handlers.Count < 3)
4688
+ {
4689
+ return;
4690
+ }
4691
+ handlers[2](ref context, ref typedMessage);
4692
+ if (handlers.Count < 4)
4693
+ {
4694
+ return;
4695
+ }
4696
+ handlers[3](ref context, ref typedMessage);
4697
+ if (handlers.Count < 5)
4698
+ {
4699
+ return;
4700
+ }
4701
+ handlers[4](ref context, ref typedMessage);
4702
+ return;
4703
+ }
4704
+ }
4705
+
4706
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
4707
+ {
4708
+ handlers[i](ref context, ref typedMessage);
4709
+ }
4710
+ }
4711
+
4712
+ private static void RunFastHandlers<TMessage>(
4713
+ ref InstanceId context,
4714
+ Dictionary<int, IHandlerActionCache> fastHandlers,
4715
+ ref TMessage message,
4716
+ int priority,
4717
+ long emissionId
4718
+ )
4719
+ where TMessage : IMessage
4720
+ {
4721
+ if (fastHandlers is not { Count: > 0 })
4722
+ {
4723
+ return;
4724
+ }
4725
+
4726
+ if (
4727
+ !fastHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
4728
+ || erasedCache is not HandlerActionCache<FastHandlerWithContext<T>> cache
4729
+ )
4730
+ {
4731
+ return;
4732
+ }
4733
+
4734
+ RunFastHandlers(ref context, cache, ref message, emissionId);
4735
+ }
4736
+
4737
+ private static void RunFastHandlers<TMessage, TU>(
4738
+ ref InstanceId context,
4739
+ Dictionary<int, IHandlerActionCache> fastHandlers,
4740
+ ref TMessage message,
4741
+ int priority,
4742
+ long emissionId
4743
+ )
4744
+ where TMessage : IMessage
4745
+ where TU : IMessage
4746
+ {
4747
+ if (fastHandlers is not { Count: > 0 })
4748
+ {
4749
+ return;
4750
+ }
4751
+
4752
+ if (
4753
+ !fastHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
4754
+ || erasedCache is not HandlerActionCache<FastHandlerWithContext<TU>> cache
4755
+ )
3993
4756
  {
3994
- handlers[i](ref typedMessage);
4757
+ return;
3995
4758
  }
4759
+
4760
+ RunFastHandlers(ref context, cache, ref message, emissionId);
3996
4761
  }
3997
4762
 
3998
4763
  private static void RunFastHandlers<TMessage, TU>(
3999
4764
  ref InstanceId context,
4000
- HandlerActionCache<FastHandlerWithContext<TU>> cache,
4765
+ Dictionary<int, HandlerActionCache<FastHandlerWithContext<TU>>> fastHandlers,
4001
4766
  ref TMessage message,
4767
+ int priority,
4002
4768
  long emissionId
4003
4769
  )
4004
4770
  where TMessage : IMessage
4005
4771
  where TU : IMessage
4006
4772
  {
4007
- if (cache?.entries is not { Count: > 0 })
4773
+ if (fastHandlers is not { Count: > 0 })
4774
+ {
4775
+ return;
4776
+ }
4777
+
4778
+ if (
4779
+ !fastHandlers.TryGetValue(
4780
+ priority,
4781
+ out HandlerActionCache<FastHandlerWithContext<TU>> cache
4782
+ )
4783
+ )
4008
4784
  {
4009
4785
  return;
4010
4786
  }
4011
4787
 
4012
- ref TU typedMessage = ref Unsafe.As<TMessage, TU>(ref message);
4788
+ ref TU typedMessage = ref DxUnsafe.As<TMessage, TU>(ref message);
4013
4789
  List<FastHandlerWithContext<TU>> handlers = GetOrAddNewHandlerStack(
4014
4790
  cache,
4015
4791
  emissionId
@@ -4025,206 +4801,455 @@ namespace DxMessaging.Core
4025
4801
  case 2:
4026
4802
  {
4027
4803
  handlers[0](ref context, ref typedMessage);
4804
+ if (handlers.Count < 2)
4805
+ {
4806
+ return;
4807
+ }
4028
4808
  handlers[1](ref context, ref typedMessage);
4029
4809
  return;
4030
4810
  }
4031
4811
  case 3:
4032
4812
  {
4033
4813
  handlers[0](ref context, ref typedMessage);
4814
+ if (handlers.Count < 2)
4815
+ {
4816
+ return;
4817
+ }
4034
4818
  handlers[1](ref context, ref typedMessage);
4819
+ if (handlers.Count < 3)
4820
+ {
4821
+ return;
4822
+ }
4035
4823
  handlers[2](ref context, ref typedMessage);
4036
4824
  return;
4037
4825
  }
4038
4826
  case 4:
4039
4827
  {
4040
4828
  handlers[0](ref context, ref typedMessage);
4829
+ if (handlers.Count < 2)
4830
+ {
4831
+ return;
4832
+ }
4041
4833
  handlers[1](ref context, ref typedMessage);
4834
+ if (handlers.Count < 3)
4835
+ {
4836
+ return;
4837
+ }
4042
4838
  handlers[2](ref context, ref typedMessage);
4839
+ if (handlers.Count < 4)
4840
+ {
4841
+ return;
4842
+ }
4043
4843
  handlers[3](ref context, ref typedMessage);
4044
4844
  return;
4045
4845
  }
4046
4846
  case 5:
4047
4847
  {
4048
4848
  handlers[0](ref context, ref typedMessage);
4849
+ if (handlers.Count < 2)
4850
+ {
4851
+ return;
4852
+ }
4049
4853
  handlers[1](ref context, ref typedMessage);
4854
+ if (handlers.Count < 3)
4855
+ {
4856
+ return;
4857
+ }
4050
4858
  handlers[2](ref context, ref typedMessage);
4859
+ if (handlers.Count < 4)
4860
+ {
4861
+ return;
4862
+ }
4051
4863
  handlers[3](ref context, ref typedMessage);
4864
+ if (handlers.Count < 5)
4865
+ {
4866
+ return;
4867
+ }
4052
4868
  handlers[4](ref context, ref typedMessage);
4053
4869
  return;
4054
4870
  }
4055
4871
  }
4056
4872
 
4057
- for (int i = 0; i < handlersCount; ++i)
4873
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
4058
4874
  {
4059
4875
  handlers[i](ref context, ref typedMessage);
4060
4876
  }
4061
4877
  }
4062
4878
 
4063
- private static void RunFastHandlers<TMessage, TU>(
4879
+ private static void RunHandlersWithContext<TMessage>(
4064
4880
  ref InstanceId context,
4065
- Dictionary<int, HandlerActionCache<FastHandlerWithContext<TU>>> fastHandlers,
4881
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext,
4066
4882
  ref TMessage message,
4067
4883
  int priority,
4068
4884
  long emissionId
4069
4885
  )
4070
4886
  where TMessage : IMessage
4071
- where TU : IMessage
4072
4887
  {
4073
- if (fastHandlers is not { Count: > 0 })
4888
+ if (
4889
+ handlersByContext is not { Count: > 0 }
4890
+ || !handlersByContext.TryGetValue(
4891
+ context,
4892
+ out Dictionary<int, IHandlerActionCache> cache
4893
+ )
4894
+ )
4074
4895
  {
4075
4896
  return;
4076
4897
  }
4077
4898
 
4899
+ RunHandlers(cache, ref message, priority, emissionId);
4900
+ }
4901
+
4902
+ private static void RunHandlersWithContext<TMessage>(
4903
+ ref InstanceId context,
4904
+ Dictionary<
4905
+ InstanceId,
4906
+ Dictionary<int, HandlerActionCache<Action<T>>>
4907
+ > handlersByContext,
4908
+ ref TMessage message,
4909
+ int priority,
4910
+ long emissionId
4911
+ )
4912
+ where TMessage : IMessage
4913
+ {
4078
4914
  if (
4079
- !fastHandlers.TryGetValue(
4080
- priority,
4081
- out HandlerActionCache<FastHandlerWithContext<TU>> cache
4915
+ handlersByContext is not { Count: > 0 }
4916
+ || !handlersByContext.TryGetValue(
4917
+ context,
4918
+ out Dictionary<int, HandlerActionCache<Action<T>>> cache
4082
4919
  )
4083
4920
  )
4084
4921
  {
4085
4922
  return;
4086
4923
  }
4087
4924
 
4088
- ref TU typedMessage = ref Unsafe.As<TMessage, TU>(ref message);
4089
- List<FastHandlerWithContext<TU>> handlers = GetOrAddNewHandlerStack(
4090
- cache,
4091
- emissionId
4092
- );
4925
+ RunHandlers(cache, ref message, priority, emissionId);
4926
+ }
4927
+
4928
+ private static void RunHandlers<TMessage>(
4929
+ Dictionary<int, IHandlerActionCache> sortedHandlers,
4930
+ ref TMessage message,
4931
+ int priority,
4932
+ long emissionId
4933
+ )
4934
+ where TMessage : IMessage
4935
+ {
4936
+ if (sortedHandlers is not { Count: > 0 })
4937
+ {
4938
+ return;
4939
+ }
4940
+
4941
+ if (
4942
+ !sortedHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
4943
+ || erasedCache is not HandlerActionCache<Action<T>> cache
4944
+ )
4945
+ {
4946
+ return;
4947
+ }
4948
+
4949
+ List<Action<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
4950
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
4093
4951
  int handlersCount = handlers.Count;
4094
4952
  switch (handlersCount)
4095
4953
  {
4096
4954
  case 1:
4097
4955
  {
4098
- handlers[0](ref context, ref typedMessage);
4956
+ handlers[0](typedMessage);
4099
4957
  return;
4100
4958
  }
4101
4959
  case 2:
4102
4960
  {
4103
- handlers[0](ref context, ref typedMessage);
4104
- handlers[1](ref context, ref typedMessage);
4961
+ handlers[0](typedMessage);
4962
+ if (handlers.Count < 2)
4963
+ {
4964
+ return;
4965
+ }
4966
+ handlers[1](typedMessage);
4105
4967
  return;
4106
4968
  }
4107
4969
  case 3:
4108
4970
  {
4109
- handlers[0](ref context, ref typedMessage);
4110
- handlers[1](ref context, ref typedMessage);
4111
- handlers[2](ref context, ref typedMessage);
4971
+ handlers[0](typedMessage);
4972
+ if (handlers.Count < 2)
4973
+ {
4974
+ return;
4975
+ }
4976
+ handlers[1](typedMessage);
4977
+ if (handlers.Count < 3)
4978
+ {
4979
+ return;
4980
+ }
4981
+ handlers[2](typedMessage);
4112
4982
  return;
4113
4983
  }
4114
4984
  case 4:
4115
4985
  {
4116
- handlers[0](ref context, ref typedMessage);
4117
- handlers[1](ref context, ref typedMessage);
4118
- handlers[2](ref context, ref typedMessage);
4119
- handlers[3](ref context, ref typedMessage);
4986
+ handlers[0](typedMessage);
4987
+ if (handlers.Count < 2)
4988
+ {
4989
+ return;
4990
+ }
4991
+ handlers[1](typedMessage);
4992
+ if (handlers.Count < 3)
4993
+ {
4994
+ return;
4995
+ }
4996
+ handlers[2](typedMessage);
4997
+ if (handlers.Count < 4)
4998
+ {
4999
+ return;
5000
+ }
5001
+ handlers[3](typedMessage);
4120
5002
  return;
4121
5003
  }
4122
5004
  case 5:
4123
5005
  {
4124
- handlers[0](ref context, ref typedMessage);
4125
- handlers[1](ref context, ref typedMessage);
4126
- handlers[2](ref context, ref typedMessage);
4127
- handlers[3](ref context, ref typedMessage);
4128
- handlers[4](ref context, ref typedMessage);
5006
+ handlers[0](typedMessage);
5007
+ if (handlers.Count < 2)
5008
+ {
5009
+ return;
5010
+ }
5011
+ handlers[1](typedMessage);
5012
+ if (handlers.Count < 3)
5013
+ {
5014
+ return;
5015
+ }
5016
+ handlers[2](typedMessage);
5017
+ if (handlers.Count < 4)
5018
+ {
5019
+ return;
5020
+ }
5021
+ handlers[3](typedMessage);
5022
+ if (handlers.Count < 5)
5023
+ {
5024
+ return;
5025
+ }
5026
+ handlers[4](typedMessage);
4129
5027
  return;
4130
5028
  }
4131
5029
  }
4132
5030
 
4133
- for (int i = 0; i < handlersCount; ++i)
5031
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
4134
5032
  {
4135
- handlers[i](ref context, ref typedMessage);
5033
+ handlers[i](typedMessage);
4136
5034
  }
4137
5035
  }
4138
5036
 
4139
- private static void RunHandlersWithContext<TMessage>(
4140
- ref InstanceId context,
4141
- Dictionary<
4142
- InstanceId,
4143
- Dictionary<int, HandlerActionCache<Action<T>>>
4144
- > handlersByContext,
5037
+ private static void RunHandlers<TMessage>(
5038
+ Dictionary<int, HandlerActionCache<Action<T>>> sortedHandlers,
4145
5039
  ref TMessage message,
4146
5040
  int priority,
4147
5041
  long emissionId
4148
5042
  )
4149
5043
  where TMessage : IMessage
4150
5044
  {
4151
- if (
4152
- handlersByContext is not { Count: > 0 }
4153
- || !handlersByContext.TryGetValue(
4154
- context,
4155
- out Dictionary<int, HandlerActionCache<Action<T>>> cache
4156
- )
4157
- )
5045
+ if (sortedHandlers is not { Count: > 0 })
4158
5046
  {
4159
5047
  return;
4160
5048
  }
4161
5049
 
4162
- RunHandlers(cache, ref message, priority, emissionId);
5050
+ if (!sortedHandlers.TryGetValue(priority, out HandlerActionCache<Action<T>> cache))
5051
+ {
5052
+ return;
5053
+ }
5054
+
5055
+ List<Action<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
5056
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5057
+ int handlersCount = handlers.Count;
5058
+ switch (handlersCount)
5059
+ {
5060
+ case 1:
5061
+ {
5062
+ handlers[0](typedMessage);
5063
+ return;
5064
+ }
5065
+ case 2:
5066
+ {
5067
+ handlers[0](typedMessage);
5068
+ if (handlers.Count < 2)
5069
+ {
5070
+ return;
5071
+ }
5072
+ handlers[1](typedMessage);
5073
+ return;
5074
+ }
5075
+ case 3:
5076
+ {
5077
+ handlers[0](typedMessage);
5078
+ if (handlers.Count < 2)
5079
+ {
5080
+ return;
5081
+ }
5082
+ handlers[1](typedMessage);
5083
+ if (handlers.Count < 3)
5084
+ {
5085
+ return;
5086
+ }
5087
+ handlers[2](typedMessage);
5088
+ return;
5089
+ }
5090
+ case 4:
5091
+ {
5092
+ handlers[0](typedMessage);
5093
+ if (handlers.Count < 2)
5094
+ {
5095
+ return;
5096
+ }
5097
+ handlers[1](typedMessage);
5098
+ if (handlers.Count < 3)
5099
+ {
5100
+ return;
5101
+ }
5102
+ handlers[2](typedMessage);
5103
+ if (handlers.Count < 4)
5104
+ {
5105
+ return;
5106
+ }
5107
+ handlers[3](typedMessage);
5108
+ return;
5109
+ }
5110
+ case 5:
5111
+ {
5112
+ handlers[0](typedMessage);
5113
+ if (handlers.Count < 2)
5114
+ {
5115
+ return;
5116
+ }
5117
+ handlers[1](typedMessage);
5118
+ if (handlers.Count < 3)
5119
+ {
5120
+ return;
5121
+ }
5122
+ handlers[2](typedMessage);
5123
+ if (handlers.Count < 4)
5124
+ {
5125
+ return;
5126
+ }
5127
+ handlers[3](typedMessage);
5128
+ if (handlers.Count < 5)
5129
+ {
5130
+ return;
5131
+ }
5132
+ handlers[4](typedMessage);
5133
+ return;
5134
+ }
5135
+ }
5136
+
5137
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
5138
+ {
5139
+ handlers[i](typedMessage);
5140
+ }
4163
5141
  }
4164
5142
 
4165
5143
  private static void RunHandlers<TMessage>(
4166
- Dictionary<int, HandlerActionCache<Action<T>>> sortedHandlers,
5144
+ ref InstanceId context,
5145
+ Dictionary<int, IHandlerActionCache> handlers,
4167
5146
  ref TMessage message,
4168
5147
  int priority,
4169
5148
  long emissionId
4170
5149
  )
4171
5150
  where TMessage : IMessage
4172
5151
  {
4173
- if (sortedHandlers is not { Count: > 0 })
5152
+ if (handlers is not { Count: > 0 })
4174
5153
  {
4175
5154
  return;
4176
5155
  }
4177
5156
 
4178
- if (!sortedHandlers.TryGetValue(priority, out HandlerActionCache<Action<T>> cache))
5157
+ if (
5158
+ !handlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5159
+ || erasedCache is not HandlerActionCache<Action<InstanceId, T>> cache
5160
+ )
4179
5161
  {
4180
5162
  return;
4181
5163
  }
4182
5164
 
4183
- List<Action<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
4184
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
4185
- int handlersCount = handlers.Count;
5165
+ List<Action<InstanceId, T>> typedHandlers = GetOrAddNewHandlerStack(
5166
+ cache,
5167
+ emissionId
5168
+ );
5169
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5170
+ int handlersCount = typedHandlers.Count;
4186
5171
  switch (handlersCount)
4187
5172
  {
4188
5173
  case 1:
4189
5174
  {
4190
- handlers[0](typedMessage);
5175
+ typedHandlers[0](context, typedMessage);
4191
5176
  return;
4192
5177
  }
4193
5178
  case 2:
4194
5179
  {
4195
- handlers[0](typedMessage);
4196
- handlers[1](typedMessage);
5180
+ typedHandlers[0](context, typedMessage);
5181
+ if (typedHandlers.Count < 2)
5182
+ {
5183
+ return;
5184
+ }
5185
+ typedHandlers[1](context, typedMessage);
4197
5186
  return;
4198
5187
  }
4199
5188
  case 3:
4200
5189
  {
4201
- handlers[0](typedMessage);
4202
- handlers[1](typedMessage);
4203
- handlers[2](typedMessage);
5190
+ typedHandlers[0](context, typedMessage);
5191
+ if (typedHandlers.Count < 2)
5192
+ {
5193
+ return;
5194
+ }
5195
+ typedHandlers[1](context, typedMessage);
5196
+ if (typedHandlers.Count < 3)
5197
+ {
5198
+ return;
5199
+ }
5200
+ typedHandlers[2](context, typedMessage);
4204
5201
  return;
4205
5202
  }
4206
5203
  case 4:
4207
5204
  {
4208
- handlers[0](typedMessage);
4209
- handlers[1](typedMessage);
4210
- handlers[2](typedMessage);
4211
- handlers[3](typedMessage);
5205
+ typedHandlers[0](context, typedMessage);
5206
+ if (typedHandlers.Count < 2)
5207
+ {
5208
+ return;
5209
+ }
5210
+ typedHandlers[1](context, typedMessage);
5211
+ if (typedHandlers.Count < 3)
5212
+ {
5213
+ return;
5214
+ }
5215
+ typedHandlers[2](context, typedMessage);
5216
+ if (typedHandlers.Count < 4)
5217
+ {
5218
+ return;
5219
+ }
5220
+ typedHandlers[3](context, typedMessage);
4212
5221
  return;
4213
5222
  }
4214
5223
  case 5:
4215
5224
  {
4216
- handlers[0](typedMessage);
4217
- handlers[1](typedMessage);
4218
- handlers[2](typedMessage);
4219
- handlers[3](typedMessage);
4220
- handlers[4](typedMessage);
5225
+ typedHandlers[0](context, typedMessage);
5226
+ if (typedHandlers.Count < 2)
5227
+ {
5228
+ return;
5229
+ }
5230
+ typedHandlers[1](context, typedMessage);
5231
+ if (typedHandlers.Count < 3)
5232
+ {
5233
+ return;
5234
+ }
5235
+ typedHandlers[2](context, typedMessage);
5236
+ if (typedHandlers.Count < 4)
5237
+ {
5238
+ return;
5239
+ }
5240
+ typedHandlers[3](context, typedMessage);
5241
+ if (typedHandlers.Count < 5)
5242
+ {
5243
+ return;
5244
+ }
5245
+ typedHandlers[4](context, typedMessage);
4221
5246
  return;
4222
5247
  }
4223
5248
  }
4224
5249
 
4225
- for (int i = 0; i < handlersCount; ++i)
5250
+ for (int i = 0; i < handlersCount && i < typedHandlers.Count; ++i)
4226
5251
  {
4227
- handlers[i](typedMessage);
5252
+ typedHandlers[i](context, typedMessage);
4228
5253
  }
4229
5254
  }
4230
5255
 
@@ -4256,7 +5281,7 @@ namespace DxMessaging.Core
4256
5281
  cache,
4257
5282
  emissionId
4258
5283
  );
4259
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
5284
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
4260
5285
  int handlersCount = typedHandlers.Count;
4261
5286
  switch (handlersCount)
4262
5287
  {
@@ -4268,60 +5293,124 @@ namespace DxMessaging.Core
4268
5293
  case 2:
4269
5294
  {
4270
5295
  typedHandlers[0](context, typedMessage);
5296
+ if (typedHandlers.Count < 2)
5297
+ {
5298
+ return;
5299
+ }
4271
5300
  typedHandlers[1](context, typedMessage);
4272
5301
  return;
4273
5302
  }
4274
5303
  case 3:
4275
5304
  {
4276
5305
  typedHandlers[0](context, typedMessage);
5306
+ if (typedHandlers.Count < 2)
5307
+ {
5308
+ return;
5309
+ }
4277
5310
  typedHandlers[1](context, typedMessage);
5311
+ if (typedHandlers.Count < 3)
5312
+ {
5313
+ return;
5314
+ }
4278
5315
  typedHandlers[2](context, typedMessage);
4279
5316
  return;
4280
5317
  }
4281
5318
  case 4:
4282
5319
  {
4283
5320
  typedHandlers[0](context, typedMessage);
5321
+ if (typedHandlers.Count < 2)
5322
+ {
5323
+ return;
5324
+ }
4284
5325
  typedHandlers[1](context, typedMessage);
5326
+ if (typedHandlers.Count < 3)
5327
+ {
5328
+ return;
5329
+ }
4285
5330
  typedHandlers[2](context, typedMessage);
5331
+ if (typedHandlers.Count < 4)
5332
+ {
5333
+ return;
5334
+ }
4286
5335
  typedHandlers[3](context, typedMessage);
4287
5336
  return;
4288
5337
  }
4289
5338
  case 5:
4290
5339
  {
4291
5340
  typedHandlers[0](context, typedMessage);
5341
+ if (typedHandlers.Count < 2)
5342
+ {
5343
+ return;
5344
+ }
4292
5345
  typedHandlers[1](context, typedMessage);
5346
+ if (typedHandlers.Count < 3)
5347
+ {
5348
+ return;
5349
+ }
4293
5350
  typedHandlers[2](context, typedMessage);
5351
+ if (typedHandlers.Count < 4)
5352
+ {
5353
+ return;
5354
+ }
4294
5355
  typedHandlers[3](context, typedMessage);
5356
+ if (typedHandlers.Count < 5)
5357
+ {
5358
+ return;
5359
+ }
4295
5360
  typedHandlers[4](context, typedMessage);
4296
5361
  return;
4297
5362
  }
4298
5363
  }
4299
5364
 
4300
- for (int i = 0; i < handlersCount; ++i)
5365
+ for (int i = 0; i < handlersCount && i < typedHandlers.Count; ++i)
4301
5366
  {
4302
5367
  typedHandlers[i](context, typedMessage);
4303
5368
  }
4304
5369
  }
4305
5370
 
5371
+ // Mid-dispatch clear contract: the List returned here is the LIVE
5372
+ // cache.cache list, not a copy. IHandlerActionCache.Reset() (bus
5373
+ // reset / sweep eviction) clears it IN PLACE, so every dispatch
5374
+ // loop that indexes the returned list re-checks list.Count before
5375
+ // each invocation past the first (and the >5 fallback loops bound
5376
+ // on the live Count). A reset fired from inside a handler then
5377
+ // cleanly stops the in-flight bucket: no peer delegate runs and
5378
+ // nothing throws. The re-check is a single inlined List.Count
5379
+ // field read on data already in cache, so steady-state dispatch
5380
+ // cost is unchanged.
4306
5381
  internal static List<TU> GetOrAddNewHandlerStack<TU>(
4307
5382
  HandlerActionCache<TU> actionCache,
4308
5383
  long emissionId
4309
5384
  )
4310
5385
  {
5386
+ DebugAssertInsertionOrderInSync(actionCache);
4311
5387
  if (actionCache.lastSeenEmissionId != emissionId)
4312
5388
  {
4313
5389
  if (actionCache.version != actionCache.lastSeenVersion)
4314
5390
  {
5391
+ // Rebuild the dispatch snapshot from insertionOrder, NOT from
5392
+ // the entries dictionary: dictionary enumeration order permutes
5393
+ // after Remove/Add churn (freed slots are reused LIFO), while
5394
+ // insertionOrder preserves the documented first-registration
5395
+ // order for equal-priority handlers. This branch only runs on
5396
+ // registration churn (version bump), never on steady-state
5397
+ // dispatch, and allocates nothing (the pooled cache list is
5398
+ // cleared and refilled in place).
4315
5399
  List<TU> list = actionCache.cache;
4316
5400
  list.Clear();
4317
- foreach (
4318
- KeyValuePair<
4319
- TU,
4320
- HandlerActionCache<TU>.Entry
4321
- > kvp in actionCache.entries
4322
- )
5401
+ List<TU> orderedHandlers = actionCache.insertionOrder;
5402
+ int orderedCount = orderedHandlers.Count;
5403
+ for (int i = 0; i < orderedCount; ++i)
4323
5404
  {
4324
- list.Add(kvp.Value.handler);
5405
+ if (
5406
+ actionCache.entries.TryGetValue(
5407
+ orderedHandlers[i],
5408
+ out HandlerActionCache<TU>.Entry entry
5409
+ )
5410
+ )
5411
+ {
5412
+ list.Add(entry.handler);
5413
+ }
4325
5414
  }
4326
5415
  actionCache.lastSeenVersion = actionCache.version;
4327
5416
  }
@@ -4330,20 +5419,126 @@ namespace DxMessaging.Core
4330
5419
  return actionCache.cache;
4331
5420
  }
4332
5421
 
4333
- private static void PrefreezeHandlersForEmission<THandler>(
4334
- Dictionary<int, HandlerActionCache<THandler>> handlers,
4335
- int priority,
4336
- long emissionId
5422
+ // Asserts insertionOrder stays in lockstep with the entries
5423
+ // dictionary at every dispatch-snapshot read. Drift indicates a
5424
+ // mutation site of HandlerActionCache.entries that forgot to
5425
+ // mirror the change into insertionOrder (AddHandler* family,
5426
+ // deregistration closures, IHandlerActionCache.Reset). Stripped
5427
+ // in Release builds via [Conditional("DEBUG")] -- zero hot-path
5428
+ // cost.
5429
+ [Conditional("DEBUG")]
5430
+ private static void DebugAssertInsertionOrderInSync<TU>(
5431
+ HandlerActionCache<TU> actionCache
5432
+ )
5433
+ {
5434
+ System.Diagnostics.Debug.Assert(
5435
+ actionCache.insertionOrder.Count == actionCache.entries.Count,
5436
+ "HandlerActionCache.insertionOrder must mirror entries: every first "
5437
+ + "registration appends and every final deregistration removes. A "
5438
+ + "count mismatch means a mutation site skipped the insertionOrder "
5439
+ + "update and same-priority dispatch order is no longer trustworthy."
5440
+ );
5441
+ }
5442
+
5443
+ private static Action AddHandler<TU>(
5444
+ TypedGlobalSlot slot,
5445
+ TU originalHandler,
5446
+ TU augmentedHandler,
5447
+ Action deregistration,
5448
+ IMessageBus messageBus
4337
5449
  )
4338
5450
  {
5451
+ slot.lastTouchTicks =
5452
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(messageBus);
5453
+ HandlerActionCache<TU> cache = slot.cache as HandlerActionCache<TU>;
5454
+ if (cache == null)
5455
+ {
5456
+ cache = new HandlerActionCache<TU>();
5457
+ slot.cache = cache;
5458
+ }
5459
+
4339
5460
  if (
4340
- handlers != null
4341
- && handlers.TryGetValue(priority, out HandlerActionCache<THandler> cache)
5461
+ !cache.entries.TryGetValue(
5462
+ originalHandler,
5463
+ out HandlerActionCache<TU>.Entry entry
5464
+ )
4342
5465
  )
4343
5466
  {
4344
- cache.prefreezeInvocationCount++;
4345
- _ = GetOrAddNewHandlerStack(cache, emissionId);
5467
+ entry = new HandlerActionCache<TU>.Entry(augmentedHandler, 0);
5468
+ }
5469
+
5470
+ bool firstRegistration = entry.count == 0;
5471
+ entry = firstRegistration
5472
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1)
5473
+ : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5474
+
5475
+ cache.entries[originalHandler] = entry;
5476
+ if (firstRegistration)
5477
+ {
5478
+ cache.insertionOrder.Add(originalHandler);
4346
5479
  }
5480
+ cache.version++;
5481
+ if (firstRegistration)
5482
+ {
5483
+ slot.liveCount++;
5484
+ }
5485
+
5486
+ HandlerActionCache<TU> localCache = cache;
5487
+ TypedGlobalSlot localSlot = slot;
5488
+ long localSlotVersion = slot.version;
5489
+ long localResetGeneration =
5490
+ global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
5491
+
5492
+ return () =>
5493
+ {
5494
+ if (
5495
+ !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
5496
+ messageBus,
5497
+ localResetGeneration
5498
+ )
5499
+ )
5500
+ {
5501
+ return;
5502
+ }
5503
+
5504
+ if (localSlot.version != localSlotVersion)
5505
+ {
5506
+ return;
5507
+ }
5508
+
5509
+ if (
5510
+ !localCache.entries.TryGetValue(
5511
+ originalHandler,
5512
+ out HandlerActionCache<TU>.Entry localEntry
5513
+ )
5514
+ )
5515
+ {
5516
+ return;
5517
+ }
5518
+
5519
+ localCache.version++;
5520
+
5521
+ deregistration?.Invoke();
5522
+ localSlot.lastTouchTicks =
5523
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
5524
+ messageBus
5525
+ );
5526
+
5527
+ if (localEntry.count <= 1)
5528
+ {
5529
+ _ = localCache.entries.Remove(originalHandler);
5530
+ _ = localCache.insertionOrder.Remove(originalHandler);
5531
+ localCache.version++;
5532
+ localSlot.liveCount--;
5533
+ return;
5534
+ }
5535
+
5536
+ localEntry = new HandlerActionCache<TU>.Entry(
5537
+ localEntry.handler,
5538
+ localEntry.count - 1
5539
+ );
5540
+ localCache.entries[originalHandler] = localEntry;
5541
+ };
4347
5542
  }
4348
5543
 
4349
5544
  private static Action AddHandler<TU>(
@@ -4356,7 +5551,7 @@ namespace DxMessaging.Core
4356
5551
  TU augmentedHandler,
4357
5552
  Action deregistration,
4358
5553
  int priority,
4359
- long emissionId
5554
+ IMessageBus messageBus
4360
5555
  )
4361
5556
  {
4362
5557
  handlersByContext ??=
@@ -4395,6 +5590,10 @@ namespace DxMessaging.Core
4395
5590
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
4396
5591
 
4397
5592
  cache.entries[originalHandler] = entry;
5593
+ if (firstRegistration)
5594
+ {
5595
+ cache.insertionOrder.Add(originalHandler);
5596
+ }
4398
5597
  cache.version++;
4399
5598
 
4400
5599
  Dictionary<
@@ -4433,6 +5632,7 @@ namespace DxMessaging.Core
4433
5632
  if (localEntry.count <= 1)
4434
5633
  {
4435
5634
  _ = localCache.entries.Remove(originalHandler);
5635
+ _ = localCache.insertionOrder.Remove(originalHandler);
4436
5636
  localCache.version++;
4437
5637
  if (localCache.entries.Count == 0)
4438
5638
  {
@@ -4480,6 +5680,10 @@ namespace DxMessaging.Core
4480
5680
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
4481
5681
 
4482
5682
  cache.entries[originalHandler] = entry;
5683
+ if (firstRegistration)
5684
+ {
5685
+ cache.insertionOrder.Add(originalHandler);
5686
+ }
4483
5687
  cache.version++;
4484
5688
 
4485
5689
  HandlerActionCache<TU> localCache = cache;
@@ -4503,6 +5707,7 @@ namespace DxMessaging.Core
4503
5707
  if (localEntry.count <= 1)
4504
5708
  {
4505
5709
  _ = localCache.entries.Remove(originalHandler);
5710
+ _ = localCache.insertionOrder.Remove(originalHandler);
4506
5711
  localCache.version++;
4507
5712
  return;
4508
5713
  }
@@ -4548,6 +5753,10 @@ namespace DxMessaging.Core
4548
5753
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
4549
5754
 
4550
5755
  cache.entries[originalHandler] = entry;
5756
+ if (firstRegistration)
5757
+ {
5758
+ cache.insertionOrder.Add(originalHandler);
5759
+ }
4551
5760
  cache.version++;
4552
5761
 
4553
5762
  Dictionary<int, HandlerActionCache<TU>> localHandlers = handlers;
@@ -4576,6 +5785,7 @@ namespace DxMessaging.Core
4576
5785
  if (localEntry.count <= 1)
4577
5786
  {
4578
5787
  _ = localCache.entries.Remove(originalHandler);
5788
+ _ = localCache.insertionOrder.Remove(originalHandler);
4579
5789
  localCache.version++;
4580
5790
  if (localCache.entries.Count == 0)
4581
5791
  {
@@ -4597,6 +5807,148 @@ namespace DxMessaging.Core
4597
5807
  // Variant of AddHandler that preserves the priority key in the dictionary when the last entry is removed.
4598
5808
  // This ensures that during an in-flight emission (where handler stacks are already frozen),
4599
5809
  // subsequent removals do not cause lookups to fail for the current pass.
5810
+ // `flatInvoker` carries the pre-resolved flat-dispatch invoker for
5811
+ // registrations the bus-side flat snapshot consumes (untargeted
5812
+ // handle/post default handlers); see HandlerActionCache.Entry.flatInvoker.
5813
+ private Action AddHandlerPreservingPriorityKey<TU>(
5814
+ Dictionary<int, IHandlerActionCache> handlers,
5815
+ TU originalHandler,
5816
+ TU augmentedHandler,
5817
+ Action deregistration,
5818
+ int priority,
5819
+ IMessageBus messageBus,
5820
+ object flatInvoker = null
5821
+ )
5822
+ {
5823
+ if (
5824
+ !handlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5825
+ || erasedCache is not HandlerActionCache<TU> cache
5826
+ )
5827
+ {
5828
+ cache = new HandlerActionCache<TU>();
5829
+ handlers[priority] = cache;
5830
+ }
5831
+
5832
+ if (
5833
+ !cache.entries.TryGetValue(
5834
+ originalHandler,
5835
+ out HandlerActionCache<TU>.Entry entry
5836
+ )
5837
+ )
5838
+ {
5839
+ entry = new HandlerActionCache<TU>.Entry(augmentedHandler, 0);
5840
+ }
5841
+
5842
+ bool firstRegistration = entry.count == 0;
5843
+ entry = firstRegistration
5844
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1, flatInvoker)
5845
+ : new HandlerActionCache<TU>.Entry(
5846
+ entry.handler,
5847
+ entry.count + 1,
5848
+ entry.flatInvoker
5849
+ );
5850
+
5851
+ cache.entries[originalHandler] = entry;
5852
+ if (firstRegistration)
5853
+ {
5854
+ cache.insertionOrder.Add(originalHandler);
5855
+ }
5856
+ cache.version++;
5857
+ TypedSlot<T> slot = FindPrioritySlot(handlers);
5858
+ if (slot != null)
5859
+ {
5860
+ slot.lastTouchTicks =
5861
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
5862
+ messageBus
5863
+ );
5864
+ }
5865
+ if (slot != null && !slot.orderedPriorities.Contains(priority))
5866
+ {
5867
+ slot.orderedPriorities.Add(priority);
5868
+ }
5869
+ if (firstRegistration && slot != null)
5870
+ {
5871
+ slot.liveCount++;
5872
+ }
5873
+
5874
+ Dictionary<int, IHandlerActionCache> localHandlers = handlers;
5875
+ TypedSlot<T> localSlot = slot;
5876
+ long localSlotVersion = slot?.version ?? 0;
5877
+ long localResetGeneration =
5878
+ global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
5879
+
5880
+ return () =>
5881
+ {
5882
+ if (
5883
+ !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
5884
+ messageBus,
5885
+ localResetGeneration
5886
+ )
5887
+ )
5888
+ {
5889
+ return;
5890
+ }
5891
+
5892
+ if (localSlot != null && localSlot.version != localSlotVersion)
5893
+ {
5894
+ return;
5895
+ }
5896
+
5897
+ if (
5898
+ !localHandlers.TryGetValue(
5899
+ priority,
5900
+ out IHandlerActionCache localErasedCache
5901
+ ) || localErasedCache is not HandlerActionCache<TU> localCache
5902
+ )
5903
+ {
5904
+ return;
5905
+ }
5906
+
5907
+ if (
5908
+ !localCache.entries.TryGetValue(
5909
+ originalHandler,
5910
+ out HandlerActionCache<TU>.Entry localEntry
5911
+ )
5912
+ )
5913
+ {
5914
+ return;
5915
+ }
5916
+
5917
+ localCache.version++;
5918
+
5919
+ deregistration?.Invoke();
5920
+ if (localSlot != null)
5921
+ {
5922
+ localSlot.lastTouchTicks =
5923
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
5924
+ messageBus
5925
+ );
5926
+ }
5927
+
5928
+ if (localEntry.count <= 1)
5929
+ {
5930
+ _ = localCache.entries.Remove(originalHandler);
5931
+ _ = localCache.insertionOrder.Remove(originalHandler);
5932
+ localCache.version++;
5933
+ if (localSlot != null)
5934
+ {
5935
+ localSlot.liveCount--;
5936
+ }
5937
+ // Intentionally DO NOT remove the priority key here to preserve
5938
+ // the cache handle during an in-flight emission.
5939
+ return;
5940
+ }
5941
+
5942
+ localEntry = new HandlerActionCache<TU>.Entry(
5943
+ localEntry.handler,
5944
+ localEntry.count - 1,
5945
+ localEntry.flatInvoker
5946
+ );
5947
+
5948
+ localCache.entries[originalHandler] = localEntry;
5949
+ };
5950
+ }
5951
+
4600
5952
  private static Action AddHandlerPreservingPriorityKey<TU>(
4601
5953
  ref Dictionary<int, HandlerActionCache<TU>> handlers,
4602
5954
  TU originalHandler,
@@ -4630,6 +5982,10 @@ namespace DxMessaging.Core
4630
5982
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
4631
5983
 
4632
5984
  cache.entries[originalHandler] = entry;
5985
+ if (firstRegistration)
5986
+ {
5987
+ cache.insertionOrder.Add(originalHandler);
5988
+ }
4633
5989
  cache.version++;
4634
5990
 
4635
5991
  Dictionary<int, HandlerActionCache<TU>> localHandlers = handlers;
@@ -4658,6 +6014,7 @@ namespace DxMessaging.Core
4658
6014
  if (localEntry.count <= 1)
4659
6015
  {
4660
6016
  _ = localCache.entries.Remove(originalHandler);
6017
+ _ = localCache.insertionOrder.Remove(originalHandler);
4661
6018
  localCache.version++;
4662
6019
  // Intentionally DO NOT remove the priority key here to preserve
4663
6020
  // the cache handle during an in-flight emission.