com.wallstop-studios.dxmessaging 2.1.9 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +106 -65
  2. package/CHANGELOG.md.meta +7 -7
  3. package/Editor/Analyzers/BaseCallIlInspector.cs +277 -0
  4. package/Editor/Analyzers/BaseCallIlInspector.cs.meta +11 -0
  5. package/Editor/Analyzers/BaseCallLogMessageParser.cs +295 -0
  6. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +11 -0
  7. package/Editor/Analyzers/BaseCallReportAggregator.cs +308 -0
  8. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +11 -0
  9. package/Editor/Analyzers/BaseCallTypeScanner.cs +110 -0
  10. package/Editor/Analyzers/BaseCallTypeScanner.cs.meta +11 -0
  11. package/Editor/Analyzers/BaseCallTypeScannerCore.cs +562 -0
  12. package/Editor/Analyzers/BaseCallTypeScannerCore.cs.meta +11 -0
  13. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +1122 -0
  14. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +11 -0
  15. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +44 -44
  16. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +44 -44
  17. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +44 -44
  18. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +44 -44
  19. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +44 -44
  20. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  21. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +33 -0
  22. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  23. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +72 -72
  24. package/Editor/Analyzers.meta +8 -8
  25. package/Editor/AssemblyInfo.cs.meta +3 -3
  26. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +81 -0
  27. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +11 -0
  28. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +420 -0
  29. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +11 -0
  30. package/Editor/CustomEditors/MessagingComponentEditor.cs +1 -1
  31. package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +2 -2
  32. package/Editor/CustomEditors.meta +2 -2
  33. package/Editor/DxMessagingEditorInitializer.cs +1 -1
  34. package/Editor/DxMessagingEditorInitializer.cs.meta +2 -2
  35. package/Editor/DxMessagingMenu.cs.meta +11 -11
  36. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +11 -11
  37. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +190 -0
  38. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +11 -0
  39. package/Editor/Settings/DxMessagingSettings.cs +189 -0
  40. package/Editor/Settings/DxMessagingSettings.cs.meta +2 -2
  41. package/Editor/Settings/DxMessagingSettingsProvider.cs +50 -33
  42. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +2 -2
  43. package/Editor/Settings.meta +2 -2
  44. package/Editor/SetupCscRsp.cs +209 -8
  45. package/Editor/SetupCscRsp.cs.meta +2 -2
  46. package/Editor/Testing/MessagingComponentEditorHarness.cs +1 -1
  47. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +3 -3
  48. package/Editor/Testing.meta +3 -3
  49. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef +14 -14
  50. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef.meta +7 -7
  51. package/Editor.meta +8 -8
  52. package/LICENSE.md +9 -9
  53. package/LICENSE.md.meta +7 -7
  54. package/README.md +941 -900
  55. package/README.md.meta +7 -7
  56. package/Runtime/AssemblyInfo.cs +4 -0
  57. package/Runtime/AssemblyInfo.cs.meta +2 -2
  58. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +2 -2
  59. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +2 -2
  60. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs +26 -0
  61. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +11 -0
  62. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +2 -2
  63. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +2 -2
  64. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +2 -2
  65. package/Runtime/Core/Attributes.meta +2 -2
  66. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs +195 -0
  67. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs.meta +11 -0
  68. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs +179 -0
  69. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs.meta +11 -0
  70. package/Runtime/Core/Configuration.meta +9 -0
  71. package/Runtime/Core/DataStructure/CyclicBuffer.cs +2 -2
  72. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +2 -2
  73. package/Runtime/Core/DataStructure.meta +2 -2
  74. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +2 -2
  75. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +2 -2
  76. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +2 -2
  77. package/Runtime/Core/Diagnostics.meta +2 -2
  78. package/Runtime/Core/DxMessagingStaticState.cs +19 -0
  79. package/Runtime/Core/DxMessagingStaticState.cs.meta +11 -11
  80. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +2 -2
  81. package/Runtime/Core/Extensions/IListExtensions.cs.meta +2 -2
  82. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +12 -12
  83. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +11 -11
  84. package/Runtime/Core/Extensions.meta +8 -8
  85. package/Runtime/Core/Helper/MessageCache.cs +32 -0
  86. package/Runtime/Core/Helper/MessageCache.cs.meta +2 -2
  87. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +2 -2
  88. package/Runtime/Core/Helper.meta +2 -2
  89. package/Runtime/Core/IMessage.cs +3 -3
  90. package/Runtime/Core/IMessage.cs.meta +11 -11
  91. package/Runtime/Core/InstanceId.cs.meta +11 -11
  92. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs +51 -0
  93. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs.meta +11 -0
  94. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs +38 -0
  95. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs.meta +11 -0
  96. package/Runtime/Core/Internal/TypedSlotIndex.cs +81 -0
  97. package/Runtime/Core/Internal/TypedSlotIndex.cs.meta +11 -0
  98. package/Runtime/Core/Internal/TypedSlots.cs +613 -0
  99. package/Runtime/Core/Internal/TypedSlots.cs.meta +11 -0
  100. package/Runtime/Core/Internal.meta +9 -0
  101. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +11 -11
  102. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +11 -11
  103. package/Runtime/Core/MessageBus/IMessageBus.cs +177 -3
  104. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +11 -11
  105. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +11 -11
  106. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +11 -11
  107. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs +16 -0
  108. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs.meta +11 -0
  109. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs +40 -0
  110. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs.meta +11 -0
  111. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +718 -0
  112. package/Runtime/Core/MessageBus/Internal/BusSlots.cs.meta +11 -0
  113. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs +38 -0
  114. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs.meta +11 -0
  115. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs +20 -0
  116. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs.meta +11 -0
  117. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs +28 -0
  118. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs.meta +11 -0
  119. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs +48 -0
  120. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs.meta +11 -0
  121. package/Runtime/Core/MessageBus/Internal/ISweepable.cs +15 -0
  122. package/Runtime/Core/MessageBus/Internal/ISweepable.cs.meta +11 -0
  123. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs +222 -0
  124. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs.meta +11 -0
  125. package/Runtime/Core/MessageBus/Internal/SlotKey.cs +192 -0
  126. package/Runtime/Core/MessageBus/Internal/SlotKey.cs.meta +11 -0
  127. package/Runtime/Core/MessageBus/Internal.meta +9 -0
  128. package/Runtime/Core/MessageBus/MessageBus.cs +2651 -500
  129. package/Runtime/Core/MessageBus/MessageBus.cs.meta +11 -11
  130. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +11 -11
  131. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +11 -11
  132. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +11 -11
  133. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +11 -11
  134. package/Runtime/Core/MessageBus.meta +8 -8
  135. package/Runtime/Core/MessageHandler.cs +2019 -542
  136. package/Runtime/Core/MessageHandler.cs.meta +11 -11
  137. package/Runtime/Core/MessageRegistrationHandle.cs.meta +11 -11
  138. package/Runtime/Core/MessageRegistrationToken.cs +7 -0
  139. package/Runtime/Core/MessageRegistrationToken.cs.meta +11 -11
  140. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +2 -2
  141. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +11 -11
  142. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +11 -11
  143. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +11 -11
  144. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +2 -2
  145. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +11 -11
  146. package/Runtime/Core/Messages/StringMessage.cs.meta +2 -2
  147. package/Runtime/Core/Messages.meta +8 -8
  148. package/Runtime/Core/MessagingDebug.cs.meta +11 -11
  149. package/Runtime/Core/Pooling/CollectionPool.cs +266 -0
  150. package/Runtime/Core/Pooling/CollectionPool.cs.meta +11 -0
  151. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs +30 -0
  152. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs.meta +11 -0
  153. package/Runtime/Core/Pooling/DxPools.cs +157 -0
  154. package/Runtime/Core/Pooling/DxPools.cs.meta +11 -0
  155. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs +106 -0
  156. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs.meta +11 -0
  157. package/Runtime/Core/Pooling/IDxMessagingClock.cs +18 -0
  158. package/Runtime/Core/Pooling/IDxMessagingClock.cs.meta +11 -0
  159. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs +55 -0
  160. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs.meta +11 -0
  161. package/Runtime/Core/Pooling/StopwatchClock.cs +27 -0
  162. package/Runtime/Core/Pooling/StopwatchClock.cs.meta +11 -0
  163. package/Runtime/Core/Pooling/UnityRealtimeClock.cs +31 -0
  164. package/Runtime/Core/Pooling/UnityRealtimeClock.cs.meta +11 -0
  165. package/Runtime/Core/Pooling.meta +9 -0
  166. package/Runtime/Core.meta +8 -8
  167. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +12 -12
  168. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +11 -11
  169. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +12 -12
  170. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +2 -2
  171. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs +73 -0
  172. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +11 -11
  173. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef +20 -20
  174. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef.meta +7 -7
  175. package/Runtime/Unity/Integrations/Reflex.meta +8 -8
  176. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +2 -2
  177. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs +109 -1
  178. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +11 -11
  179. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef +30 -30
  180. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef.meta +7 -7
  181. package/Runtime/Unity/Integrations/VContainer.meta +8 -8
  182. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +2 -2
  183. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef +30 -30
  184. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef.meta +7 -7
  185. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs +79 -1
  186. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +11 -11
  187. package/Runtime/Unity/Integrations/Zenject.meta +8 -8
  188. package/Runtime/Unity/Integrations.meta +8 -8
  189. package/Runtime/Unity/MessageAwareComponent.cs +29 -0
  190. package/Runtime/Unity/MessageAwareComponent.cs.meta +11 -11
  191. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +12 -12
  192. package/Runtime/Unity/MessagingComponent.cs.meta +11 -11
  193. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +12 -12
  194. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +12 -12
  195. package/Runtime/Unity.meta +8 -8
  196. package/Runtime/WallstopStudios.DxMessaging.asmdef +14 -14
  197. package/Runtime/WallstopStudios.DxMessaging.asmdef.meta +7 -7
  198. package/Runtime.meta +8 -8
  199. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab +98 -98
  200. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab.meta +7 -7
  201. package/Samples~/DI/Prefabs.meta +8 -8
  202. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset +14 -14
  203. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset.meta +8 -8
  204. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset +14 -14
  205. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset.meta +8 -8
  206. package/Samples~/DI/Providers.meta +8 -8
  207. package/Samples~/DI/README.md +51 -51
  208. package/Samples~/DI/README.md.meta +7 -7
  209. package/Samples~/DI/Reflex/SampleInstaller.cs +7 -0
  210. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +11 -11
  211. package/Samples~/DI/Reflex.meta +8 -8
  212. package/Samples~/DI/VContainer/SampleLifetimeScope.cs +6 -1
  213. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +11 -11
  214. package/Samples~/DI/VContainer.meta +8 -8
  215. package/Samples~/DI/Zenject/SampleInstaller.cs +8 -0
  216. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +11 -11
  217. package/Samples~/DI/Zenject.meta +8 -8
  218. package/Samples~/DI.meta +8 -8
  219. package/Samples~/Mini Combat/Boot.cs.meta +11 -11
  220. package/Samples~/Mini Combat/Enemy.cs.meta +11 -11
  221. package/Samples~/Mini Combat/Messages.cs.meta +11 -11
  222. package/Samples~/Mini Combat/Player.cs.meta +11 -11
  223. package/Samples~/Mini Combat/README.md +324 -323
  224. package/Samples~/Mini Combat/README.md.meta +7 -7
  225. package/Samples~/Mini Combat/UIOverlay.cs.meta +11 -11
  226. package/Samples~/Mini Combat/Walkthrough.md +430 -430
  227. package/Samples~/Mini Combat/Walkthrough.md.meta +7 -7
  228. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef +13 -13
  229. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef.meta +7 -7
  230. package/Samples~/Mini Combat.meta +8 -8
  231. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +11 -11
  232. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +11 -11
  233. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +11 -11
  234. package/Samples~/UI Buttons + Inspector/README.md +210 -209
  235. package/Samples~/UI Buttons + Inspector/README.md.meta +7 -7
  236. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +11 -11
  237. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef +13 -13
  238. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef.meta +7 -7
  239. package/Samples~/UI Buttons + Inspector.meta +8 -8
  240. package/SourceGenerators/Directory.Build.props.meta +7 -7
  241. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +11 -11
  242. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +2 -2
  243. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj.meta +7 -7
  244. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.meta +8 -8
  245. package/SourceGenerators.meta +8 -8
  246. package/Third Party Notices.md +3 -3
  247. package/Third Party Notices.md.meta +7 -7
  248. package/package.json +115 -92
  249. package/package.json.meta +7 -7
@@ -3,6 +3,7 @@ namespace DxMessaging.Core.MessageBus
3
3
  using System;
4
4
  using System.Buffers;
5
5
  using System.Collections.Generic;
6
+ using System.Diagnostics;
6
7
  using System.Linq.Expressions;
7
8
  using System.Reflection;
8
9
  using System.Runtime.CompilerServices;
@@ -11,9 +12,12 @@ namespace DxMessaging.Core.MessageBus
11
12
  using DxMessaging.Core;
12
13
  using Extensions;
13
14
  using Helper;
15
+ using Internal;
14
16
  using Messages;
17
+ using Pooling;
15
18
  using static IMessageBus;
16
19
  #if UNITY_2021_3_OR_NEWER
20
+ using Configuration;
17
21
  using UnityEngine;
18
22
  #endif
19
23
 
@@ -24,8 +28,9 @@ namespace DxMessaging.Core.MessageBus
24
28
  {
25
29
  private long _emissionId;
26
30
  public long EmissionId => _emissionId;
31
+ internal long TickCounter => _tickCounter;
27
32
 
28
- private readonly struct PrefreezeDescriptor
33
+ internal readonly struct PrefreezeDescriptor
29
34
  {
30
35
  public PrefreezeDescriptor(byte kind, int priority)
31
36
  {
@@ -38,37 +43,120 @@ namespace DxMessaging.Core.MessageBus
38
43
  public readonly int priority;
39
44
  }
40
45
 
41
- private enum DispatchCategory : byte
42
- {
43
- None = 0,
44
- Untargeted = 1,
45
- UntargetedPost = 2,
46
- Targeted = 3,
47
- TargetedPost = 4,
48
- TargetedWithoutTargeting = 5,
49
- TargetedWithoutTargetingPost = 6,
50
- Broadcast = 7,
51
- BroadcastPost = 8,
52
- BroadcastWithoutSource = 9,
53
- BroadcastWithoutSourcePost = 10,
54
- GlobalUntargeted = 11,
55
- GlobalTargeted = 12,
56
- GlobalBroadcast = 13,
57
- }
58
-
59
46
  private const byte PrefreezeKindNone = 0;
60
47
  private const byte PrefreezeKindTargetedWithoutTargetingHandlers = 1;
61
48
  private const byte PrefreezeKindBroadcastWithoutSourceHandlers = 2;
62
49
  private const byte PrefreezeKindGlobalUntargetedHandlers = 3;
63
50
  private const byte PrefreezeKindGlobalTargetedHandlers = 4;
64
51
  private const byte PrefreezeKindGlobalBroadcastHandlers = 5;
52
+ private const long DefaultIdleEvictionTicks = 30;
53
+ private const double DefaultEvictionTickIntervalSeconds = 5d;
54
+ internal const int SweepGateSampleSize = 16;
55
+ private const long SweepGateMask = SweepGateSampleSize - 1;
56
+
57
+ private static readonly SlotKey UntargetedHandleSlot = new SlotKey(
58
+ DispatchKind.Untargeted,
59
+ DispatchPhase.Handle,
60
+ DispatchVariant.Default
61
+ );
62
+ private static readonly SlotKey UntargetedPostSlot = new SlotKey(
63
+ DispatchKind.Untargeted,
64
+ DispatchPhase.PostProcess,
65
+ DispatchVariant.Default
66
+ );
67
+ private static readonly SlotKey TargetedHandleSlot = new SlotKey(
68
+ DispatchKind.Targeted,
69
+ DispatchPhase.Handle,
70
+ DispatchVariant.Default
71
+ );
72
+ private static readonly SlotKey TargetedWithoutContextHandleSlot = new SlotKey(
73
+ DispatchKind.Targeted,
74
+ DispatchPhase.Handle,
75
+ DispatchVariant.WithoutContext
76
+ );
77
+ private static readonly SlotKey TargetedPostSlot = new SlotKey(
78
+ DispatchKind.Targeted,
79
+ DispatchPhase.PostProcess,
80
+ DispatchVariant.Default
81
+ );
82
+ private static readonly SlotKey TargetedWithoutContextPostSlot = new SlotKey(
83
+ DispatchKind.Targeted,
84
+ DispatchPhase.PostProcess,
85
+ DispatchVariant.WithoutContext
86
+ );
87
+ private static readonly SlotKey BroadcastPostSlot = new SlotKey(
88
+ DispatchKind.Broadcast,
89
+ DispatchPhase.PostProcess,
90
+ DispatchVariant.Default
91
+ );
92
+ private static readonly SlotKey BroadcastWithoutContextPostSlot = new SlotKey(
93
+ DispatchKind.Broadcast,
94
+ DispatchPhase.PostProcess,
95
+ DispatchVariant.WithoutContext
96
+ );
97
+ internal const int ExpectedMessageCacheFieldCount = 5;
98
+
99
+ private static readonly ISweepable[] SweepableTypeCacheRegistry =
100
+ {
101
+ new SweepableTypeCache(
102
+ nameof(_scalarSinks),
103
+ typeof(MessageCache<HandlerCache<int, HandlerCache>>[]),
104
+ static (bus, force) => bus.SweepDirtyScalarTypeSlots(force)
105
+ ),
106
+ new SweepableTypeCache(
107
+ nameof(_contextSinks),
108
+ typeof(MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>>[]),
109
+ static (bus, force) => bus.SweepDirtyTargetSlots(force)
110
+ ),
111
+ new SweepableTypeCache(
112
+ nameof(_untargetedInterceptsByType),
113
+ typeof(MessageCache<InterceptorCache<object>>),
114
+ static (bus, force) =>
115
+ bus.SweepDirtyInterceptorTypeSlots(bus._untargetedInterceptsByType, force)
116
+ ),
117
+ new SweepableTypeCache(
118
+ nameof(_targetedInterceptsByType),
119
+ typeof(MessageCache<InterceptorCache<object>>),
120
+ static (bus, force) =>
121
+ bus.SweepDirtyInterceptorTypeSlots(bus._targetedInterceptsByType, force)
122
+ ),
123
+ new SweepableTypeCache(
124
+ nameof(_broadcastInterceptsByType),
125
+ typeof(MessageCache<InterceptorCache<object>>),
126
+ static (bus, force) =>
127
+ bus.SweepDirtyInterceptorTypeSlots(bus._broadcastInterceptsByType, force)
128
+ ),
129
+ };
130
+
131
+ internal static IReadOnlyList<ISweepable> SweepableTypeCaches => SweepableTypeCacheRegistry;
65
132
 
66
133
  private static readonly ArrayPool<DispatchBucket> DispatchBucketPool =
67
134
  ArrayPool<DispatchBucket>.Shared;
68
135
  private static readonly ArrayPool<DispatchEntry> DispatchEntryPool =
69
136
  ArrayPool<DispatchEntry>.Shared;
70
137
 
71
- private readonly struct DispatchEntry
138
+ private static CollectionPool<
139
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
140
+ > ContextHandlerByTargetDicts => ContextHandlerByTargetDictPoolHolder.Instance;
141
+
142
+ private static class ContextHandlerByTargetDictPoolHolder
143
+ {
144
+ public static readonly CollectionPool<
145
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
146
+ > Instance = new(
147
+ maxRetained: 512,
148
+ useLru: true,
149
+ factory: static () => new Dictionary<InstanceId, HandlerCache<int, HandlerCache>>(),
150
+ onRecycled: static dict => dict.Clear()
151
+ );
152
+ }
153
+
154
+ internal static int ResetStaticPools()
155
+ {
156
+ return ContextHandlerByTargetDicts.Trim(0);
157
+ }
158
+
159
+ internal readonly struct DispatchEntry
72
160
  {
73
161
  public DispatchEntry(
74
162
  MessageHandler handler,
@@ -86,7 +174,7 @@ namespace DxMessaging.Core.MessageBus
86
174
  public readonly PrefreezeDescriptor prefreeze;
87
175
  }
88
176
 
89
- private struct DispatchBucket
177
+ internal struct DispatchBucket
90
178
  {
91
179
  public DispatchBucket(
92
180
  int priority,
@@ -127,7 +215,7 @@ namespace DxMessaging.Core.MessageBus
127
215
  }
128
216
  }
129
217
 
130
- private sealed class DispatchSnapshot
218
+ internal sealed class DispatchSnapshot
131
219
  {
132
220
  public static readonly DispatchSnapshot Empty = new DispatchSnapshot(
133
221
  Array.Empty<DispatchBucket>(),
@@ -169,281 +257,1740 @@ namespace DxMessaging.Core.MessageBus
169
257
  }
170
258
  }
171
259
 
172
- private sealed class HandlerCache<TKey, TValue>
260
+ internal sealed class DispatchState
173
261
  {
174
- internal sealed class DispatchState
175
- {
176
- public DispatchSnapshot active = DispatchSnapshot.Empty;
177
- public DispatchSnapshot pending = DispatchSnapshot.Empty;
178
- public bool hasPending;
179
- public bool pendingDirty;
180
- public long snapshotEmissionId = -1;
262
+ public DispatchSnapshot active = DispatchSnapshot.Empty;
263
+ public DispatchSnapshot pending = DispatchSnapshot.Empty;
264
+ public bool hasPending;
265
+ public bool pendingDirty;
266
+ public long snapshotEmissionId = -1;
181
267
 
182
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
183
- public void Reset()
184
- {
185
- ReleaseSnapshot(ref active);
186
- ReleaseSnapshot(ref pending);
187
- hasPending = false;
188
- pendingDirty = false;
189
- snapshotEmissionId = -1;
190
- }
268
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
269
+ public void Reset()
270
+ {
271
+ ReleaseSnapshot(ref active);
272
+ ReleaseSnapshot(ref pending);
273
+ hasPending = false;
274
+ pendingDirty = false;
275
+ snapshotEmissionId = -1;
191
276
  }
277
+ }
192
278
 
279
+ private sealed class HandlerCache<TKey, TValue>
280
+ {
193
281
  public readonly Dictionary<TKey, TValue> handlers = new();
194
282
  public readonly List<TKey> order = new();
195
283
  public readonly List<KeyValuePair<TKey, TValue>> cache = new();
196
284
  public long version;
197
285
  public long lastSeenVersion = -1;
198
- public long lastSeenEmissionId;
199
- private readonly Dictionary<DispatchCategory, DispatchState> _dispatchStates = new();
286
+ public long lastSeenEmissionId = -1;
287
+ public long lastTouchTicks;
288
+ public DispatchState dispatchState;
200
289
 
201
290
  /// <summary>
202
291
  /// Clears all cached handler references and resets the version tracking metadata.
203
292
  /// </summary>
204
293
  public void Clear()
205
294
  {
295
+ // LEGACY: version reset semantics. Bus-side deregistration closures use
296
+ // captured cache identity and reset generations, so monotonic versioning
297
+ // is handled by sweep-driven slot reset paths.
206
298
  handlers.Clear();
207
299
  order.Clear();
208
300
  cache.Clear();
209
301
  version = 0;
210
302
  lastSeenVersion = -1;
211
- lastSeenEmissionId = 0;
212
- if (_dispatchStates.Count > 0)
213
- {
214
- foreach (DispatchState state in _dispatchStates.Values)
215
- {
216
- state.Reset();
217
- }
218
- _dispatchStates.Clear();
219
- }
220
- }
221
-
222
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
223
- public DispatchState GetOrCreateDispatchState(DispatchCategory category)
224
- {
225
- if (!_dispatchStates.TryGetValue(category, out DispatchState state))
226
- {
227
- state = new DispatchState();
228
- _dispatchStates[category] = state;
229
- }
230
-
231
- return state;
303
+ lastSeenEmissionId = -1;
304
+ dispatchState?.Reset();
305
+ dispatchState = null;
232
306
  }
233
307
  }
234
308
 
235
309
  private sealed class InterceptorCache<TValue>
236
310
  {
237
311
  public readonly SortedList<int, List<TValue>> handlers = new();
238
- public long lastSeenEmissionId;
312
+ public long lastSeenEmissionId = -1;
313
+ public long lastTouchTicks;
239
314
 
240
315
  public void Clear()
241
316
  {
242
317
  handlers.Clear();
243
- lastSeenEmissionId = 0;
318
+ lastSeenEmissionId = -1;
319
+ lastTouchTicks = 0;
244
320
  }
245
321
  }
246
322
 
247
- private sealed class HandlerCache
323
+ private sealed class SweepableTypeCache : ISweepable
248
324
  {
249
- internal sealed class DispatchState
325
+ private readonly Func<MessageBus, bool, int> _sweep;
326
+
327
+ public SweepableTypeCache(
328
+ string storageFieldName,
329
+ Type storageFieldType,
330
+ Func<MessageBus, bool, int> sweep
331
+ )
250
332
  {
251
- public DispatchSnapshot active = DispatchSnapshot.Empty;
252
- public DispatchSnapshot pending = DispatchSnapshot.Empty;
253
- public bool hasPending;
254
- public bool pendingDirty;
255
- public long snapshotEmissionId = -1;
333
+ StorageFieldName = storageFieldName;
334
+ StorageFieldType = storageFieldType;
335
+ _sweep = sweep;
336
+ }
256
337
 
257
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
258
- public void Reset()
338
+ public string StorageFieldName { get; }
339
+ public Type StorageFieldType { get; }
340
+
341
+ public int Sweep(MessageBus bus, bool force)
342
+ {
343
+ if (bus == null)
259
344
  {
260
- ReleaseSnapshot(ref active);
261
- ReleaseSnapshot(ref pending);
262
- hasPending = false;
263
- pendingDirty = false;
264
- snapshotEmissionId = -1;
345
+ throw new ArgumentNullException(nameof(bus));
265
346
  }
347
+
348
+ return _sweep(bus, force);
349
+ }
350
+ }
351
+
352
+ private readonly struct DispatchLease : IDisposable
353
+ {
354
+ private readonly MessageBus _bus;
355
+
356
+ public DispatchLease(MessageBus bus)
357
+ {
358
+ _bus = bus;
359
+ _bus._dispatchDepth++;
360
+ }
361
+
362
+ public void Dispose()
363
+ {
364
+ _bus._dispatchDepth--;
266
365
  }
366
+ }
267
367
 
368
+ private sealed class HandlerCache
369
+ {
268
370
  public readonly Dictionary<MessageHandler, int> handlers = new();
269
371
  public readonly List<MessageHandler> cache = new();
270
372
  public long version;
271
373
  public long lastSeenVersion = -1;
272
- public long lastSeenEmissionId;
273
- private readonly Dictionary<DispatchCategory, DispatchState> _dispatchStates = new();
374
+ public long lastSeenEmissionId = -1;
274
375
 
275
376
  /// <summary>
276
377
  /// Clears all cached handler references and resets the version tracking metadata.
277
378
  /// </summary>
278
379
  public void Clear()
279
380
  {
381
+ // LEGACY: version reset semantics. Bus-side deregistration closures use
382
+ // captured cache identity and reset generations, so monotonic versioning
383
+ // is handled by sweep-driven slot reset paths.
280
384
  handlers.Clear();
281
385
  cache.Clear();
282
386
  version = 0;
283
387
  lastSeenVersion = -1;
284
- lastSeenEmissionId = 0;
285
- if (_dispatchStates.Count > 0)
388
+ lastSeenEmissionId = -1;
389
+ }
390
+ }
391
+
392
+ public int RegisteredTargeted
393
+ {
394
+ get
395
+ {
396
+ int count = 0;
397
+ count += SumTargetedSinks(_contextSinks[BusContextIndex.TargetedHandleDefault]);
398
+ foreach (
399
+ HandlerCache<int, HandlerCache> entry in _scalarSinks[
400
+ BusSinkIndex.TargetedHandleWithoutContext
401
+ ]
402
+ )
403
+ {
404
+ count += entry?.handlers?.Count ?? 0;
405
+ }
406
+
407
+ return count;
408
+ }
409
+ }
410
+
411
+ public int RegisteredGlobalSequentialIndex { get; } = GenerateNewGlobalSequentialIndex();
412
+
413
+ public int OccupiedTypeSlots
414
+ {
415
+ get
416
+ {
417
+ int count = 0;
418
+ for (int i = 0; i < _scalarSinks.Length; ++i)
419
+ {
420
+ MessageCache<HandlerCache<int, HandlerCache>> sink = _scalarSinks[i];
421
+ if (sink == null)
422
+ {
423
+ continue;
424
+ }
425
+
426
+ foreach (HandlerCache<int, HandlerCache> _ in sink)
427
+ {
428
+ count++;
429
+ }
430
+ }
431
+
432
+ for (int i = 0; i < _contextSinks.Length; ++i)
433
+ {
434
+ foreach (
435
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> _ in _contextSinks[
436
+ i
437
+ ]
438
+ )
439
+ {
440
+ count++;
441
+ }
442
+ }
443
+
444
+ return count + OccupiedInterceptorTypeSlots + CountDirtyEmptyTypedHandlerSlots();
445
+ }
446
+ }
447
+
448
+ private int OccupiedInterceptorTypeSlots
449
+ {
450
+ get
451
+ {
452
+ return CountOccupiedInterceptorTypeSlots(_untargetedInterceptsByType)
453
+ + CountOccupiedInterceptorTypeSlots(_targetedInterceptsByType)
454
+ + CountOccupiedInterceptorTypeSlots(_broadcastInterceptsByType);
455
+ }
456
+ }
457
+
458
+ public int OccupiedTargetSlots
459
+ {
460
+ get
461
+ {
462
+ int count = 0;
463
+ for (int i = 0; i < _contextSinks.Length; ++i)
286
464
  {
287
- foreach (DispatchState state in _dispatchStates.Values)
465
+ foreach (
466
+ Dictionary<
467
+ InstanceId,
468
+ HandlerCache<int, HandlerCache>
469
+ > byTarget in _contextSinks[i]
470
+ )
288
471
  {
289
- state.Reset();
472
+ count += byTarget?.Count ?? 0;
290
473
  }
291
- _dispatchStates.Clear();
292
474
  }
475
+
476
+ return count;
293
477
  }
478
+ }
294
479
 
295
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
296
- public DispatchState GetOrCreateDispatchState(DispatchCategory category)
480
+ public int RegisteredBroadcast
481
+ {
482
+ get
483
+ {
484
+ int count = 0;
485
+ count += SumTargetedSinks(_contextSinks[BusContextIndex.BroadcastHandleDefault]);
486
+ foreach (
487
+ HandlerCache<int, HandlerCache> entry in _scalarSinks[
488
+ BusSinkIndex.BroadcastHandleWithoutContext
489
+ ]
490
+ )
491
+ {
492
+ count += entry?.handlers?.Count ?? 0;
493
+ }
494
+
495
+ return count;
496
+ }
497
+ }
498
+
499
+ public int RegisteredUntargeted
500
+ {
501
+ get
502
+ {
503
+ int count = 0;
504
+ foreach (
505
+ HandlerCache<int, HandlerCache> entry in _scalarSinks[
506
+ BusSinkIndex.UntargetedHandleDefault
507
+ ]
508
+ )
509
+ {
510
+ count += entry?.handlers?.Count ?? 0;
511
+ }
512
+
513
+ return count;
514
+ }
515
+ }
516
+
517
+ public int RegisteredInterceptors
518
+ {
519
+ get
520
+ {
521
+ int count = 0;
522
+ count += SumInterceptorCache(_untargetedInterceptsByType);
523
+ count += SumInterceptorCache(_targetedInterceptsByType);
524
+ count += SumInterceptorCache(_broadcastInterceptsByType);
525
+ return count;
526
+ }
527
+ }
528
+
529
+ public int RegisteredPostProcessors
530
+ {
531
+ get
532
+ {
533
+ int count = 0;
534
+ foreach (
535
+ HandlerCache<int, HandlerCache> entry in _scalarSinks[
536
+ BusSinkIndex.UntargetedPostProcessDefault
537
+ ]
538
+ )
539
+ {
540
+ count += entry?.handlers?.Count ?? 0;
541
+ }
542
+ count += SumTargetedSinks(
543
+ _contextSinks[BusContextIndex.TargetedPostProcessDefault]
544
+ );
545
+ count += SumTargetedSinks(
546
+ _contextSinks[BusContextIndex.BroadcastPostProcessDefault]
547
+ );
548
+ foreach (
549
+ HandlerCache<int, HandlerCache> entry in _scalarSinks[
550
+ BusSinkIndex.TargetedPostProcessWithoutContext
551
+ ]
552
+ )
553
+ {
554
+ count += entry?.handlers?.Count ?? 0;
555
+ }
556
+ foreach (
557
+ HandlerCache<int, HandlerCache> entry in _scalarSinks[
558
+ BusSinkIndex.BroadcastPostProcessWithoutContext
559
+ ]
560
+ )
561
+ {
562
+ count += entry?.handlers?.Count ?? 0;
563
+ }
564
+ return count;
565
+ }
566
+ }
567
+
568
+ public int RegisteredGlobalAcceptAll => _globalSlots.sharedHandlers.Count;
569
+
570
+ private static int SumInterceptorCache(MessageCache<InterceptorCache<object>> cache)
571
+ {
572
+ int count = 0;
573
+ foreach (InterceptorCache<object> entry in cache)
574
+ {
575
+ if (entry == null)
576
+ {
577
+ continue;
578
+ }
579
+ foreach (KeyValuePair<int, List<object>> bucket in entry.handlers)
580
+ {
581
+ count += bucket.Value?.Count ?? 0;
582
+ }
583
+ }
584
+ return count;
585
+ }
586
+
587
+ private static int SumTargetedSinks(
588
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> cache
589
+ )
590
+ {
591
+ int count = 0;
592
+ foreach (Dictionary<InstanceId, HandlerCache<int, HandlerCache>> entry in cache)
297
593
  {
298
- if (!_dispatchStates.TryGetValue(category, out DispatchState state))
594
+ if (entry == null)
595
+ {
596
+ continue;
597
+ }
598
+ foreach (KeyValuePair<InstanceId, HandlerCache<int, HandlerCache>> kvp in entry)
299
599
  {
300
- state = new DispatchState();
301
- _dispatchStates[category] = state;
600
+ count += kvp.Value?.handlers?.Count ?? 0;
302
601
  }
602
+ }
603
+ return count;
604
+ }
605
+
606
+ public bool DiagnosticsMode
607
+ {
608
+ get => _diagnosticsMode;
609
+ set => _diagnosticsMode = value;
610
+ }
611
+
612
+ private static readonly Type MessageBusType = typeof(MessageBus);
613
+
614
+ // For use with re-broadcasting to generic methods
615
+ private static readonly object[] ReflectionMethodArgumentsCache = new object[2];
616
+ private static readonly List<Expression> ArgumentExpressionsCache = new();
617
+
618
+ private const BindingFlags ReflectionHelperBindingFlags =
619
+ BindingFlags.Static | BindingFlags.NonPublic;
620
+ private const BindingFlags ReflexiveMethodBindingFlags =
621
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
622
+
623
+ private delegate void FastUntargetedBroadcast<T>(ref T message)
624
+ where T : IUntargetedMessage;
625
+ private delegate void FastTargetedBroadcast<T>(ref InstanceId target, ref T message)
626
+ where T : ITargetedMessage;
627
+ private delegate void FastSourcedBroadcast<T>(ref InstanceId target, ref T message)
628
+ where T : IBroadcastMessage;
629
+
630
+ public RegistrationLog Log => _log;
631
+
632
+ // Storage trio for typed and global dispatch. _scalarSinks and
633
+ // _contextSinks are SlotKey-indexed arrays of MessageCache (call sites
634
+ // index by BusSinkIndex / BusContextIndex constants; reserved-null
635
+ // entries are documented in BusSinkIndex.cs). _globalSlots is a single
636
+ // BusGlobalSlot -- the global accept-all slot is single-cardinality, so
637
+ // there is no array to index, but it is grouped here because it shares
638
+ // the lifecycle of the typed sinks (cleared together in ResetState,
639
+ // touched together by the eviction layer).
640
+ private readonly MessageCache<HandlerCache<int, HandlerCache>>[] _scalarSinks =
641
+ new MessageCache<HandlerCache<int, HandlerCache>>[BusSinkIndex.Length]
642
+ {
643
+ /* [0] UntargetedHandleDefault */new(),
644
+ /* [1] BroadcastHandleWithoutContext */new(),
645
+ /* [2] TargetedHandleWithoutContext */new(),
646
+ /* [3] UntargetedPostProcessDefault */new(),
647
+ /* [4] TargetedPostProcessWithoutContext */new(),
648
+ /* [5] BroadcastPostProcessWithoutContext */new(),
649
+ /* [6] Reserved6 */null,
650
+ /* [7] Reserved7 */null,
651
+ };
652
+
653
+ private readonly MessageCache<
654
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
655
+ >[] _contextSinks = new MessageCache<
656
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
657
+ >[BusContextIndex.Length]
658
+ {
659
+ /* [0] TargetedHandleDefault */new(),
660
+ /* [1] BroadcastHandleDefault */new(),
661
+ /* [2] TargetedPostProcessDefault */new(),
662
+ /* [3] BroadcastPostProcessDefault */new(),
663
+ };
664
+
665
+ private readonly BusGlobalSlot _globalSlots = new();
666
+
667
+ /// <summary>
668
+ /// Constructs a <see cref="MessageBus"/> using the default <see cref="StopwatchClock"/>
669
+ /// and runtime-settings provided eviction cadence. This is the only public constructor; DI
670
+ /// containers that scan constructors reflectively (for example VContainer, which inspects
671
+ /// both public and private constructors) must be configured with an explicit factory --
672
+ /// see the integration helpers under <c>Runtime/Unity/Integrations</c>.
673
+ /// </summary>
674
+ public MessageBus()
675
+ : this(StopwatchClock.Instance, DefaultIdleEvictionTicks, applyRuntimeSettings: true)
676
+ { }
677
+
678
+ /// <summary>
679
+ /// Internal factory used by tests and integration assemblies to construct a
680
+ /// <see cref="MessageBus"/> with an injected <see cref="IDxMessagingClock"/> and optional
681
+ /// eviction overrides. Lives behind an <c>internal static</c> entry point so the public
682
+ /// surface exposes only the parameterless constructor; this keeps reflection-based DI
683
+ /// containers from latching onto a clock-taking overload they cannot satisfy.
684
+ /// </summary>
685
+ /// <param name="clock">Clock implementation. Must not be null.</param>
686
+ /// <param name="idleEvictionTicks">Optional idle-eviction tick budget; falls back to <see cref="DefaultIdleEvictionTicks"/> when null.</param>
687
+ /// <param name="evictionTickIntervalSeconds">Optional sweep cadence in seconds.</param>
688
+ /// <param name="idleEvictionEnabled">Optional opt-out for idle eviction.</param>
689
+ /// <param name="trimApiEnabled">Optional opt-out for the trim API.</param>
690
+ /// <returns>Configured <see cref="MessageBus"/> instance.</returns>
691
+ internal static MessageBus CreateForInternalUse(
692
+ IDxMessagingClock clock,
693
+ long? idleEvictionTicks = null,
694
+ double? evictionTickIntervalSeconds = null,
695
+ bool? idleEvictionEnabled = null,
696
+ bool? trimApiEnabled = null
697
+ )
698
+ {
699
+ if (clock == null)
700
+ {
701
+ throw new ArgumentNullException(nameof(clock));
702
+ }
703
+
704
+ long resolvedIdleEvictionTicks = idleEvictionTicks ?? DefaultIdleEvictionTicks;
705
+ bool applyRuntimeSettings =
706
+ idleEvictionTicks == null
707
+ && evictionTickIntervalSeconds == null
708
+ && idleEvictionEnabled == null
709
+ && trimApiEnabled == null;
710
+
711
+ MessageBus bus = new MessageBus(
712
+ clock,
713
+ resolvedIdleEvictionTicks,
714
+ applyRuntimeSettings: applyRuntimeSettings
715
+ );
716
+
717
+ if (evictionTickIntervalSeconds.HasValue)
718
+ {
719
+ bus._evictionTickIntervalSeconds = Math.Max(0d, evictionTickIntervalSeconds.Value);
720
+ }
721
+ if (idleEvictionEnabled.HasValue)
722
+ {
723
+ bus._idleEvictionEnabled = idleEvictionEnabled.Value;
724
+ }
725
+ if (trimApiEnabled.HasValue)
726
+ {
727
+ bus._trimApiEnabled = trimApiEnabled.Value;
728
+ }
729
+
730
+ return bus;
731
+ }
732
+
733
+ private MessageBus(
734
+ IDxMessagingClock clock,
735
+ long idleEvictionTicks,
736
+ bool applyRuntimeSettings
737
+ )
738
+ {
739
+ _clock = clock ?? throw new ArgumentNullException(nameof(clock));
740
+ _idleEvictionTicks = Math.Max(0, idleEvictionTicks);
741
+ _evictionTickIntervalSeconds = DefaultEvictionTickIntervalSeconds;
742
+ _lastSweepSeconds = _clock.NowSeconds;
743
+ #if UNITY_2021_3_OR_NEWER
744
+ RegisterForIdleSweeps(this);
745
+ EnsureRuntimeSettingsSubscription();
746
+ if (applyRuntimeSettings)
747
+ {
748
+ ApplyRuntimeSettings(DxMessagingRuntimeSettingsProvider.Current);
749
+ }
750
+ #endif
751
+ ValidateSinkArrays();
752
+ }
303
753
 
304
- return state;
754
+ #if UNITY_2021_3_OR_NEWER
755
+ private static readonly List<WeakReference<MessageBus>> IdleSweepBuses = new();
756
+ private static bool RuntimeSettingsSubscribed;
757
+
758
+ private static void RegisterForIdleSweeps(MessageBus bus)
759
+ {
760
+ for (int i = IdleSweepBuses.Count - 1; i >= 0; --i)
761
+ {
762
+ if (!IdleSweepBuses[i].TryGetTarget(out MessageBus existing))
763
+ {
764
+ IdleSweepBuses.RemoveAt(i);
765
+ continue;
766
+ }
767
+ if (ReferenceEquals(existing, bus))
768
+ {
769
+ return;
770
+ }
771
+ }
772
+
773
+ IdleSweepBuses.Add(new WeakReference<MessageBus>(bus));
774
+ }
775
+
776
+ private static void EnsureRuntimeSettingsSubscription()
777
+ {
778
+ if (RuntimeSettingsSubscribed)
779
+ {
780
+ return;
781
+ }
782
+
783
+ DxMessagingRuntimeSettings.SettingsChanged += HandleRuntimeSettingsChanged;
784
+ RuntimeSettingsSubscribed = true;
785
+ }
786
+
787
+ private static void HandleRuntimeSettingsChanged(DxMessagingRuntimeSettings settings)
788
+ {
789
+ if (settings == null)
790
+ {
791
+ settings = DxMessagingRuntimeSettingsProvider.Current;
792
+ }
793
+
794
+ for (int i = IdleSweepBuses.Count - 1; i >= 0; --i)
795
+ {
796
+ if (IdleSweepBuses[i].TryGetTarget(out MessageBus bus))
797
+ {
798
+ bus.ApplyRuntimeSettings(settings);
799
+ continue;
800
+ }
801
+
802
+ IdleSweepBuses.RemoveAt(i);
803
+ }
804
+ }
805
+
806
+ internal static void SweepIdleBusesFromPlayerLoop()
807
+ {
808
+ for (int i = IdleSweepBuses.Count - 1; i >= 0; --i)
809
+ {
810
+ if (IdleSweepBuses[i].TryGetTarget(out MessageBus bus))
811
+ {
812
+ bus.TrySweepIdle(advanceTickForIdleAging: true);
813
+ continue;
814
+ }
815
+
816
+ IdleSweepBuses.RemoveAt(i);
817
+ }
818
+ }
819
+
820
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
821
+ private static void ResetIdleSweepRegistry()
822
+ {
823
+ DxMessagingRuntimeSettings.SettingsChanged -= HandleRuntimeSettingsChanged;
824
+ IdleSweepBuses.Clear();
825
+ RuntimeSettingsSubscribed = false;
826
+ ResetStaticPools();
827
+ }
828
+
829
+ private void ApplyRuntimeSettings(DxMessagingRuntimeSettings settings)
830
+ {
831
+ if (settings == null)
832
+ {
833
+ return;
834
+ }
835
+
836
+ DxPools.Configure(settings);
837
+ ContextHandlerByTargetDicts.UseLru = settings.BufferUseLruEviction;
838
+ ContextHandlerByTargetDicts.MaxRetained = settings.BufferMaxDistinctEntries;
839
+ if (!settings.IsFallbackInstance)
840
+ {
841
+ IMessageBus.GlobalMessageBufferSize = Math.Max(0, settings.MessageBufferSize);
842
+ }
843
+ _emissionBuffer.Resize(Math.Max(0, IMessageBus.GlobalMessageBufferSize));
844
+ _idleEvictionTicks = ComputeIdleEvictionTicks(settings.IdleEvictionSeconds);
845
+ _evictionTickIntervalSeconds = Math.Max(0d, settings.EvictionTickIntervalSeconds);
846
+ _idleEvictionEnabled = settings.EvictionEnabled;
847
+ _trimApiEnabled = settings.EnableTrimApi;
848
+ }
849
+ #endif
850
+
851
+ private static long ComputeIdleEvictionTicks(float idleEvictionSeconds)
852
+ {
853
+ if (idleEvictionSeconds <= 0f)
854
+ {
855
+ return 0;
856
+ }
857
+
858
+ return (long)Math.Ceiling(idleEvictionSeconds);
859
+ }
860
+
861
+ [Conditional("DEBUG")]
862
+ private void ValidateSinkArrays()
863
+ {
864
+ if (_scalarSinks.Length != BusSinkIndex.Length)
865
+ {
866
+ throw new InvalidOperationException(
867
+ $"_scalarSinks length is {_scalarSinks.Length} but BusSinkIndex.Length is {BusSinkIndex.Length}."
868
+ );
869
+ }
870
+ if (_contextSinks.Length != BusContextIndex.Length)
871
+ {
872
+ throw new InvalidOperationException(
873
+ $"_contextSinks length is {_contextSinks.Length} but BusContextIndex.Length is {BusContextIndex.Length}."
874
+ );
875
+ }
876
+ if (_scalarSinks[BusSinkIndex.Reserved6] != null)
877
+ {
878
+ throw new InvalidOperationException(
879
+ "_scalarSinks[Reserved6] is a permanent future-expansion stub and must be null."
880
+ );
881
+ }
882
+ if (_scalarSinks[BusSinkIndex.Reserved7] != null)
883
+ {
884
+ throw new InvalidOperationException(
885
+ "_scalarSinks[Reserved7] is a permanent future-expansion stub and must be null."
886
+ );
887
+ }
888
+ if (_scalarSinks[BusSinkIndex.UntargetedHandleDefault] == null)
889
+ {
890
+ throw new InvalidOperationException(
891
+ "_scalarSinks[UntargetedHandleDefault] must be non-null."
892
+ );
893
+ }
894
+ if (_scalarSinks[BusSinkIndex.BroadcastHandleWithoutContext] == null)
895
+ {
896
+ throw new InvalidOperationException(
897
+ "_scalarSinks[BroadcastHandleWithoutContext] must be non-null."
898
+ );
899
+ }
900
+ if (_scalarSinks[BusSinkIndex.TargetedHandleWithoutContext] == null)
901
+ {
902
+ throw new InvalidOperationException(
903
+ "_scalarSinks[TargetedHandleWithoutContext] must be non-null."
904
+ );
905
+ }
906
+ if (_scalarSinks[BusSinkIndex.UntargetedPostProcessDefault] == null)
907
+ {
908
+ throw new InvalidOperationException(
909
+ "_scalarSinks[UntargetedPostProcessDefault] must be non-null."
910
+ );
911
+ }
912
+ if (_scalarSinks[BusSinkIndex.TargetedPostProcessWithoutContext] == null)
913
+ {
914
+ throw new InvalidOperationException(
915
+ "_scalarSinks[TargetedPostProcessWithoutContext] must be non-null."
916
+ );
917
+ }
918
+ if (_scalarSinks[BusSinkIndex.BroadcastPostProcessWithoutContext] == null)
919
+ {
920
+ throw new InvalidOperationException(
921
+ "_scalarSinks[BroadcastPostProcessWithoutContext] must be non-null."
922
+ );
923
+ }
924
+ if (_contextSinks[BusContextIndex.TargetedHandleDefault] == null)
925
+ {
926
+ throw new InvalidOperationException(
927
+ "_contextSinks[TargetedHandleDefault] must be non-null."
928
+ );
929
+ }
930
+ if (_contextSinks[BusContextIndex.BroadcastHandleDefault] == null)
931
+ {
932
+ throw new InvalidOperationException(
933
+ "_contextSinks[BroadcastHandleDefault] must be non-null."
934
+ );
935
+ }
936
+ if (_contextSinks[BusContextIndex.TargetedPostProcessDefault] == null)
937
+ {
938
+ throw new InvalidOperationException(
939
+ "_contextSinks[TargetedPostProcessDefault] must be non-null."
940
+ );
941
+ }
942
+ if (_contextSinks[BusContextIndex.BroadcastPostProcessDefault] == null)
943
+ {
944
+ throw new InvalidOperationException(
945
+ "_contextSinks[BroadcastPostProcessDefault] must be non-null."
946
+ );
947
+ }
948
+ }
949
+
950
+ // Asserts BusGlobalSlot.liveCount remains in lockstep with
951
+ // _globalSlots.sharedHandlers.Count after every register / deregister.
952
+ // Stripped in Release builds via [Conditional("DEBUG")] -- zero
953
+ // hot-path cost. Kept separate from ValidateSinkArrays (which runs
954
+ // once at construction) because this invariant must hold across
955
+ // mutations, not only at startup.
956
+ [Conditional("DEBUG")]
957
+ private void DebugAssertGlobalLiveCount()
958
+ {
959
+ System.Diagnostics.Debug.Assert(
960
+ _globalSlots.liveCount == _globalSlots.sharedHandlers.Count,
961
+ "BusGlobalSlot.liveCount must mirror sharedHandlers.Count at every "
962
+ + "stable observation point. Drift indicates a missed register / "
963
+ + "deregister wiring point or an unexpected mutation path."
964
+ );
965
+ }
966
+
967
+ // Interceptors split by category to avoid mixing types
968
+ private readonly MessageCache<InterceptorCache<object>> _untargetedInterceptsByType = new();
969
+ private readonly MessageCache<InterceptorCache<object>> _targetedInterceptsByType = new();
970
+ private readonly MessageCache<InterceptorCache<object>> _broadcastInterceptsByType = new();
971
+ private readonly Dictionary<object, Dictionary<int, int>> _uniqueInterceptorsAndPriorities =
972
+ new();
973
+
974
+ private readonly Dictionary<Type, object> _broadcastMethodsByType = new();
975
+ private readonly Stack<List<object>> _innerInterceptorsStack = new();
976
+
977
+ private readonly Dictionary<
978
+ Type,
979
+ Dictionary<MethodSignatureKey, Action<MonoBehaviour, object[]>>
980
+ > _methodCache = new();
981
+
982
+ #if UNITY_2021_3_OR_NEWER
983
+ private readonly HashSet<MonoBehaviour> _recipientCache = new();
984
+ private readonly List<MonoBehaviour> _componentCache = new();
985
+ #endif
986
+
987
+ private readonly RegistrationLog _log = new();
988
+ internal readonly CyclicBuffer<MessageEmissionData> _emissionBuffer = new(
989
+ GlobalMessageBufferSize
990
+ );
991
+
992
+ private bool _diagnosticsMode = ShouldEnableDiagnostics();
993
+ private bool _loggedReflexiveWarning;
994
+ private long _tickCounter;
995
+ private readonly IDxMessagingClock _clock;
996
+ private long _idleEvictionTicks = DefaultIdleEvictionTicks;
997
+ private double _evictionTickIntervalSeconds = DefaultEvictionTickIntervalSeconds;
998
+ private bool _idleEvictionEnabled = true;
999
+ private bool _trimApiEnabled = true;
1000
+ private double _lastSweepSeconds;
1001
+ private readonly List<int> _dirtyTypes = new();
1002
+ private readonly Dictionary<int, List<InstanceId>> _dirtyTargets = new();
1003
+ private readonly Dictionary<int, int> _dirtyTargetHighWaterCounts = new();
1004
+ private readonly HashSet<int> _dirtyTypeSet = new();
1005
+ private readonly Dictionary<int, HashSet<InstanceId>> _dirtyTargetSets = new();
1006
+ private readonly Dictionary<
1007
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>,
1008
+ int
1009
+ > _contextMapHighWaterCounts = new();
1010
+ private readonly List<MessageHandler> _dirtyHandlers = new();
1011
+ private readonly HashSet<MessageHandler> _dirtyHandlerSet = new();
1012
+ private readonly Dictionary<MessageHandler, long> _dirtyHandlerTicks = new();
1013
+ private bool _globalSlotSweepCandidate;
1014
+ private long _globalSlotSweepGeneration;
1015
+ private int _lastContextTypeSlotsEvicted;
1016
+ private int _dispatchDepth;
1017
+
1018
+ // Bumped by ResetState. Deregister closures captured before the bump
1019
+ // compare their captured generation to this field and silently skip
1020
+ // when they no longer match, so a deferred Object.Destroy that lands
1021
+ // after a Reset cannot log spurious over-deregistration errors.
1022
+ private long _resetGeneration;
1023
+
1024
+ /// <summary>
1025
+ /// Bumps the internal reset generation counter without clearing any registrations or sinks.
1026
+ /// </summary>
1027
+ /// <remarks>
1028
+ /// <para>
1029
+ /// Deregister closures returned by the registration entry points capture the value of the
1030
+ /// reset generation at registration time and silently no-op when the captured value differs
1031
+ /// from the bus's current value. Calling this method invalidates every previously-issued
1032
+ /// deregister closure for this bus, which is the desired behaviour after a logical "wipe"
1033
+ /// performed by external state-management code (for example, a custom domain-reload-disabled
1034
+ /// reset utility) that does not wish to clear registrations via <see cref="ResetState"/>.
1035
+ /// </para>
1036
+ /// <para>
1037
+ /// <see cref="DxMessagingStaticState.Reset"/> uses this method to extend the destroy-then-Reset
1038
+ /// race-safety guarantee to user-installed custom global buses without clobbering their state.
1039
+ /// </para>
1040
+ /// </remarks>
1041
+ public void BumpResetGeneration()
1042
+ {
1043
+ unchecked
1044
+ {
1045
+ _resetGeneration++;
1046
+ }
1047
+ }
1048
+
1049
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1050
+ internal static long GetCurrentTouchTick(IMessageBus messageBus)
1051
+ {
1052
+ return messageBus is MessageBus bus ? bus._tickCounter : messageBus?.EmissionId ?? 0;
1053
+ }
1054
+
1055
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1056
+ internal static long GetResetGeneration(IMessageBus messageBus)
1057
+ {
1058
+ return messageBus is MessageBus bus ? bus._resetGeneration : 0;
1059
+ }
1060
+
1061
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1062
+ internal static bool IsResetGenerationCurrent(IMessageBus messageBus, long generation)
1063
+ {
1064
+ return messageBus is not MessageBus bus || bus._resetGeneration == generation;
1065
+ }
1066
+
1067
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1068
+ private long AdvanceTick()
1069
+ {
1070
+ unchecked
1071
+ {
1072
+ _tickCounter++;
1073
+ }
1074
+
1075
+ return _tickCounter;
1076
+ }
1077
+
1078
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1079
+ private static void Touch(HandlerCache<int, HandlerCache> handlers, long tick)
1080
+ {
1081
+ if (handlers != null)
1082
+ {
1083
+ handlers.lastTouchTicks = tick;
1084
+ }
1085
+ }
1086
+
1087
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1088
+ private void MarkDirtyType<TMessage>()
1089
+ where TMessage : IMessage
1090
+ {
1091
+ int typeIndex = MessageHelperIndexer<TMessage>.SequentialId;
1092
+ if (0 <= typeIndex && _dirtyTypeSet.Add(typeIndex))
1093
+ {
1094
+ _dirtyTypes.Add(typeIndex);
1095
+ }
1096
+ }
1097
+
1098
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1099
+ private void MarkDirtyTarget<TMessage>(InstanceId target)
1100
+ where TMessage : IMessage
1101
+ {
1102
+ int typeIndex = MessageHelperIndexer<TMessage>.SequentialId;
1103
+ if (typeIndex < 0)
1104
+ {
1105
+ return;
1106
+ }
1107
+
1108
+ if (!_dirtyTargets.TryGetValue(typeIndex, out List<InstanceId> targets))
1109
+ {
1110
+ targets = DxPools.InstanceIdLists.Rent();
1111
+ _dirtyTargets[typeIndex] = targets;
1112
+ }
1113
+
1114
+ if (!_dirtyTargetSets.TryGetValue(typeIndex, out HashSet<InstanceId> targetSet))
1115
+ {
1116
+ targetSet = DxPools.InstanceIdSets.Rent();
1117
+ _dirtyTargetSets[typeIndex] = targetSet;
1118
+ }
1119
+
1120
+ if (targetSet.Add(target))
1121
+ {
1122
+ targets.Add(target);
1123
+ _dirtyTargetHighWaterCounts[typeIndex] = Math.Max(
1124
+ GetDirtyTargetHighWaterCount(typeIndex),
1125
+ targets.Count
1126
+ );
1127
+ }
1128
+ }
1129
+
1130
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1131
+ private void MarkDirtyHandler(MessageHandler handler)
1132
+ {
1133
+ if (handler == null)
1134
+ {
1135
+ return;
1136
+ }
1137
+
1138
+ _dirtyHandlerTicks[handler] = _tickCounter;
1139
+ if (_dirtyHandlerSet.Add(handler))
1140
+ {
1141
+ _dirtyHandlers.Add(handler);
1142
+ }
1143
+ }
1144
+
1145
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1146
+ private DispatchLease EnterDispatch()
1147
+ {
1148
+ return new DispatchLease(this);
1149
+ }
1150
+
1151
+ public TrimResult Trim(bool force = false)
1152
+ {
1153
+ if (!_trimApiEnabled)
1154
+ {
1155
+ return default;
1156
+ }
1157
+
1158
+ return Sweep(force);
1159
+ }
1160
+
1161
+ internal TrimResult Sweep(bool force)
1162
+ {
1163
+ int typeSlotsEvicted = SweepableTypeCacheRegistry[0].Sweep(this, force);
1164
+ _lastContextTypeSlotsEvicted = 0;
1165
+ int targetSlotsEvicted = SweepableTypeCacheRegistry[1].Sweep(this, force);
1166
+ typeSlotsEvicted += _lastContextTypeSlotsEvicted;
1167
+ typeSlotsEvicted += SweepableTypeCacheRegistry[2].Sweep(this, force);
1168
+ typeSlotsEvicted += SweepableTypeCacheRegistry[3].Sweep(this, force);
1169
+ typeSlotsEvicted += SweepableTypeCacheRegistry[4].Sweep(this, force);
1170
+ typeSlotsEvicted += SweepGlobalSlot(force);
1171
+ typeSlotsEvicted += SweepDirtyTypedHandlerSlots(force);
1172
+ if (force)
1173
+ {
1174
+ ClearDirtySweepCandidates();
1175
+ }
1176
+ else
1177
+ {
1178
+ PruneDirtySweepCandidates();
1179
+ }
1180
+ int pooledCollectionsEvicted = DxPools.TrimAll(force);
1181
+ pooledCollectionsEvicted += ContextHandlerByTargetDicts.Trim(
1182
+ force ? 0 : ContextHandlerByTargetDicts.MaxRetained
1183
+ );
1184
+ _lastSweepSeconds = _clock.NowSeconds;
1185
+
1186
+ return new TrimResult(
1187
+ typeSlotsEvicted,
1188
+ targetSlotsEvicted,
1189
+ pooledCollectionsEvicted,
1190
+ OccupiedTypeSlots
1191
+ );
1192
+ }
1193
+
1194
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1195
+ private void TrySweepIdle(bool advanceTickForIdleAging = false)
1196
+ {
1197
+ if (!_idleEvictionEnabled)
1198
+ {
1199
+ return;
1200
+ }
1201
+
1202
+ if (!advanceTickForIdleAging && ((unchecked(_emissionId + 1)) & SweepGateMask) != 0)
1203
+ {
1204
+ return;
1205
+ }
1206
+
1207
+ double nowSeconds = _clock.NowSeconds;
1208
+ if (nowSeconds - _lastSweepSeconds < _evictionTickIntervalSeconds)
1209
+ {
1210
+ return;
1211
+ }
1212
+
1213
+ if (advanceTickForIdleAging)
1214
+ {
1215
+ _ = AdvanceTick();
1216
+ }
1217
+
1218
+ _ = Sweep(force: false);
1219
+ }
1220
+
1221
+ private int SweepDirtyScalarTypeSlots(bool force)
1222
+ {
1223
+ int evicted = 0;
1224
+ for (int i = 0; i < _dirtyTypes.Count; ++i)
1225
+ {
1226
+ int typeIndex = _dirtyTypes[i];
1227
+ for (int sinkIndex = 0; sinkIndex < _scalarSinks.Length; ++sinkIndex)
1228
+ {
1229
+ MessageCache<HandlerCache<int, HandlerCache>> sink = _scalarSinks[sinkIndex];
1230
+ if (
1231
+ sink == null
1232
+ || !sink.TryGetValueAtIndex(
1233
+ typeIndex,
1234
+ out HandlerCache<int, HandlerCache> handlers
1235
+ )
1236
+ || handlers.handlers.Count != 0
1237
+ || HasActiveDispatchSnapshot(handlers.dispatchState)
1238
+ || !IsIdleForSweep(handlers.lastTouchTicks, force)
1239
+ )
1240
+ {
1241
+ continue;
1242
+ }
1243
+
1244
+ handlers.Clear();
1245
+ sink.RemoveAtIndex(typeIndex);
1246
+ evicted++;
1247
+ }
1248
+ }
1249
+
1250
+ return evicted;
1251
+ }
1252
+
1253
+ private int SweepDirtyInterceptorTypeSlots(
1254
+ MessageCache<InterceptorCache<object>> interceptorsByType,
1255
+ bool force
1256
+ )
1257
+ {
1258
+ int evicted = 0;
1259
+ for (int i = 0; i < _dirtyTypes.Count; ++i)
1260
+ {
1261
+ int typeIndex = _dirtyTypes[i];
1262
+ if (
1263
+ !interceptorsByType.TryGetValueAtIndex(
1264
+ typeIndex,
1265
+ out InterceptorCache<object> interceptors
1266
+ )
1267
+ || interceptors.handlers.Count != 0
1268
+ || !IsIdleForSweep(interceptors.lastTouchTicks, force)
1269
+ )
1270
+ {
1271
+ continue;
1272
+ }
1273
+
1274
+ interceptors.Clear();
1275
+ interceptorsByType.RemoveAtIndex(typeIndex);
1276
+ evicted++;
1277
+ }
1278
+
1279
+ return evicted;
1280
+ }
1281
+
1282
+ private int SweepDirtyTargetSlots(bool force)
1283
+ {
1284
+ int evicted = 0;
1285
+ foreach (KeyValuePair<int, List<InstanceId>> dirtyTargetEntry in _dirtyTargets)
1286
+ {
1287
+ int typeIndex = dirtyTargetEntry.Key;
1288
+ List<InstanceId> targets = dirtyTargetEntry.Value;
1289
+ for (int sinkIndex = 0; sinkIndex < _contextSinks.Length; ++sinkIndex)
1290
+ {
1291
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sink =
1292
+ _contextSinks[sinkIndex];
1293
+ if (
1294
+ sink == null
1295
+ || !sink.TryGetValueAtIndex(
1296
+ typeIndex,
1297
+ out Dictionary<
1298
+ InstanceId,
1299
+ HandlerCache<int, HandlerCache>
1300
+ > handlersByTarget
1301
+ )
1302
+ )
1303
+ {
1304
+ continue;
1305
+ }
1306
+
1307
+ for (int targetIndex = 0; targetIndex < targets.Count; ++targetIndex)
1308
+ {
1309
+ InstanceId target = targets[targetIndex];
1310
+ if (
1311
+ !handlersByTarget.TryGetValue(
1312
+ target,
1313
+ out HandlerCache<int, HandlerCache> handlers
1314
+ )
1315
+ || handlers.handlers.Count != 0
1316
+ || HasActiveDispatchSnapshot(handlers.dispatchState)
1317
+ || !IsIdleForSweep(handlers.lastTouchTicks, force)
1318
+ )
1319
+ {
1320
+ continue;
1321
+ }
1322
+
1323
+ handlers.Clear();
1324
+ _ = handlersByTarget.Remove(target);
1325
+ evicted++;
1326
+ }
1327
+
1328
+ if (handlersByTarget.Count == 0)
1329
+ {
1330
+ RemoveAndReturnContextMap(sink, typeIndex, handlersByTarget);
1331
+ _lastContextTypeSlotsEvicted++;
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ return evicted;
1337
+ }
1338
+
1339
+ private int SweepGlobalSlot(bool force)
1340
+ {
1341
+ if (
1342
+ !_globalSlotSweepCandidate
1343
+ || !_globalSlots.IsEmpty
1344
+ || HasActiveGlobalDispatchSnapshot()
1345
+ || !IsIdleForSweep(_globalSlots.lastTouchTicks, force)
1346
+ )
1347
+ {
1348
+ return 0;
1349
+ }
1350
+
1351
+ // LEGACY: global slot reset keeps the sweep-generation guard for stale
1352
+ // deregistration closures.
1353
+ _globalSlots.Reset();
1354
+ unchecked
1355
+ {
1356
+ _globalSlotSweepGeneration++;
1357
+ }
1358
+ _globalSlotSweepCandidate = false;
1359
+ return 1;
1360
+ }
1361
+
1362
+ private int SweepDirtyTypedHandlerSlots(bool force)
1363
+ {
1364
+ int evicted = 0;
1365
+ if (_dispatchDepth > 0)
1366
+ {
1367
+ return evicted;
1368
+ }
1369
+
1370
+ int write = 0;
1371
+ int count = _dirtyHandlers.Count;
1372
+ for (int i = 0; i < count; ++i)
1373
+ {
1374
+ MessageHandler handler = _dirtyHandlers[i];
1375
+ if (
1376
+ !force
1377
+ && (
1378
+ !_dirtyHandlerTicks.TryGetValue(handler, out long lastTouchTicks)
1379
+ || !IsIdleForSweep(lastTouchTicks, force: false)
1380
+ )
1381
+ )
1382
+ {
1383
+ _dirtyHandlers[write++] = handler;
1384
+ continue;
1385
+ }
1386
+
1387
+ evicted += handler.ResetEmptyTypedSlotsForSweep(this);
1388
+ if (handler.HasTypedHandlersForBus(this))
1389
+ {
1390
+ _dirtyHandlers[write++] = handler;
1391
+ continue;
1392
+ }
1393
+
1394
+ _dirtyHandlerSet.Remove(handler);
1395
+ _dirtyHandlerTicks.Remove(handler);
1396
+ }
1397
+
1398
+ if (write < count)
1399
+ {
1400
+ _dirtyHandlers.RemoveRange(write, count - write);
1401
+ }
1402
+
1403
+ return evicted;
1404
+ }
1405
+
1406
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1407
+ private bool IsIdleForSweep(long lastTouchTicks, bool force)
1408
+ {
1409
+ return force || unchecked(_tickCounter - lastTouchTicks) > _idleEvictionTicks;
1410
+ }
1411
+
1412
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1413
+ private bool HasActiveDispatchSnapshot(DispatchState state)
1414
+ {
1415
+ return _dispatchDepth > 0 && state != null && !state.active.IsEmpty;
1416
+ }
1417
+
1418
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
1419
+ private bool HasActiveGlobalDispatchSnapshot()
1420
+ {
1421
+ return HasActiveDispatchSnapshot(_globalSlots.untargetedDispatchState)
1422
+ || HasActiveDispatchSnapshot(_globalSlots.targetedDispatchState)
1423
+ || HasActiveDispatchSnapshot(_globalSlots.broadcastDispatchState);
1424
+ }
1425
+
1426
+ private void PruneDirtySweepCandidates()
1427
+ {
1428
+ PruneDirtyScalarTypeCandidates();
1429
+ PruneDirtyTargetCandidates();
1430
+ PruneDirtyHandlerCandidates();
1431
+ }
1432
+
1433
+ private void PruneDirtyScalarTypeCandidates()
1434
+ {
1435
+ int write = 0;
1436
+ for (int i = 0; i < _dirtyTypes.Count; ++i)
1437
+ {
1438
+ int typeIndex = _dirtyTypes[i];
1439
+ if (
1440
+ HasFreshEmptyScalarTypeCandidate(typeIndex)
1441
+ || HasFreshEmptyInterceptorTypeCandidate(typeIndex)
1442
+ )
1443
+ {
1444
+ _dirtyTypes[write++] = typeIndex;
1445
+ continue;
1446
+ }
1447
+
1448
+ _dirtyTypeSet.Remove(typeIndex);
1449
+ }
1450
+
1451
+ if (write < _dirtyTypes.Count)
1452
+ {
1453
+ _dirtyTypes.RemoveRange(write, _dirtyTypes.Count - write);
1454
+ }
1455
+ }
1456
+
1457
+ private bool HasFreshEmptyScalarTypeCandidate(int typeIndex)
1458
+ {
1459
+ for (int sinkIndex = 0; sinkIndex < _scalarSinks.Length; ++sinkIndex)
1460
+ {
1461
+ MessageCache<HandlerCache<int, HandlerCache>> sink = _scalarSinks[sinkIndex];
1462
+ if (
1463
+ sink != null
1464
+ && sink.TryGetValueAtIndex(
1465
+ typeIndex,
1466
+ out HandlerCache<int, HandlerCache> handlers
1467
+ )
1468
+ && handlers.handlers.Count == 0
1469
+ && !IsIdleForSweep(handlers.lastTouchTicks, force: false)
1470
+ )
1471
+ {
1472
+ return true;
1473
+ }
1474
+ }
1475
+
1476
+ return false;
1477
+ }
1478
+
1479
+ private bool HasFreshEmptyInterceptorTypeCandidate(int typeIndex)
1480
+ {
1481
+ return HasFreshEmptyInterceptorTypeCandidate(_untargetedInterceptsByType, typeIndex)
1482
+ || HasFreshEmptyInterceptorTypeCandidate(_targetedInterceptsByType, typeIndex)
1483
+ || HasFreshEmptyInterceptorTypeCandidate(_broadcastInterceptsByType, typeIndex);
1484
+ }
1485
+
1486
+ private bool HasFreshEmptyInterceptorTypeCandidate(
1487
+ MessageCache<InterceptorCache<object>> interceptorsByType,
1488
+ int typeIndex
1489
+ )
1490
+ {
1491
+ return interceptorsByType.TryGetValueAtIndex(
1492
+ typeIndex,
1493
+ out InterceptorCache<object> interceptors
1494
+ )
1495
+ && interceptors.handlers.Count == 0
1496
+ && !IsIdleForSweep(interceptors.lastTouchTicks, force: false);
1497
+ }
1498
+
1499
+ private void PruneDirtyTargetCandidates()
1500
+ {
1501
+ List<int> emptyTypeKeys = null;
1502
+ foreach (KeyValuePair<int, List<InstanceId>> entry in _dirtyTargets)
1503
+ {
1504
+ int typeIndex = entry.Key;
1505
+ List<InstanceId> targets = entry.Value;
1506
+ _dirtyTargetSets.TryGetValue(typeIndex, out HashSet<InstanceId> targetSet);
1507
+ int write = 0;
1508
+ for (int i = 0; i < targets.Count; ++i)
1509
+ {
1510
+ InstanceId target = targets[i];
1511
+ if (HasFreshEmptyTargetCandidate(typeIndex, target))
1512
+ {
1513
+ targets[write++] = target;
1514
+ continue;
1515
+ }
1516
+
1517
+ targetSet?.Remove(target);
1518
+ }
1519
+
1520
+ if (write < targets.Count)
1521
+ {
1522
+ targets.RemoveRange(write, targets.Count - write);
1523
+ }
1524
+
1525
+ if (targets.Count == 0)
1526
+ {
1527
+ (emptyTypeKeys ??= new List<int>()).Add(typeIndex);
1528
+ }
1529
+ }
1530
+
1531
+ if (emptyTypeKeys == null)
1532
+ {
1533
+ return;
1534
+ }
1535
+
1536
+ for (int i = 0; i < emptyTypeKeys.Count; ++i)
1537
+ {
1538
+ int typeIndex = emptyTypeKeys[i];
1539
+ ReturnDirtyTargetCollections(typeIndex);
1540
+ _dirtyTargets.Remove(typeIndex);
1541
+ _dirtyTargetSets.Remove(typeIndex);
1542
+ }
1543
+ }
1544
+
1545
+ private bool HasFreshEmptyTargetCandidate(int typeIndex, InstanceId target)
1546
+ {
1547
+ for (int sinkIndex = 0; sinkIndex < _contextSinks.Length; ++sinkIndex)
1548
+ {
1549
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sink =
1550
+ _contextSinks[sinkIndex];
1551
+ if (
1552
+ sink == null
1553
+ || !sink.TryGetValueAtIndex(
1554
+ typeIndex,
1555
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
1556
+ )
1557
+ || !handlersByTarget.TryGetValue(
1558
+ target,
1559
+ out HandlerCache<int, HandlerCache> handlers
1560
+ )
1561
+ )
1562
+ {
1563
+ continue;
1564
+ }
1565
+
1566
+ if (
1567
+ handlers.handlers.Count == 0
1568
+ && (
1569
+ HasActiveDispatchSnapshot(handlers.dispatchState)
1570
+ || !IsIdleForSweep(handlers.lastTouchTicks, force: false)
1571
+ )
1572
+ )
1573
+ {
1574
+ return true;
1575
+ }
1576
+ }
1577
+
1578
+ return false;
1579
+ }
1580
+
1581
+ private void PruneDirtyHandlerCandidates()
1582
+ {
1583
+ int write = 0;
1584
+ for (int i = 0; i < _dirtyHandlers.Count; ++i)
1585
+ {
1586
+ MessageHandler handler = _dirtyHandlers[i];
1587
+ if (
1588
+ handler != null
1589
+ && _dirtyHandlerSet.Contains(handler)
1590
+ && _dirtyHandlerTicks.TryGetValue(handler, out long lastTouchTicks)
1591
+ && handler.CountEmptyTypedSlotsForSweep(this) > 0
1592
+ && !IsIdleForSweep(lastTouchTicks, force: false)
1593
+ )
1594
+ {
1595
+ _dirtyHandlers[write++] = handler;
1596
+ continue;
1597
+ }
1598
+
1599
+ _dirtyHandlerSet.Remove(handler);
1600
+ _dirtyHandlerTicks.Remove(handler);
1601
+ }
1602
+
1603
+ if (write < _dirtyHandlers.Count)
1604
+ {
1605
+ _dirtyHandlers.RemoveRange(write, _dirtyHandlers.Count - write);
1606
+ }
1607
+ }
1608
+
1609
+ private void ClearDirtySweepCandidates()
1610
+ {
1611
+ ClearDirtyTypeCandidatesWithoutEmptySlots();
1612
+ ClearDirtyTargetCandidatesWithoutEmptySlots();
1613
+ ClearDirtyHandlerCandidatesWithoutEmptySlots();
1614
+ }
1615
+
1616
+ private void ReturnDirtyTargetCollections(int typeIndex)
1617
+ {
1618
+ _dirtyTargets.TryGetValue(typeIndex, out List<InstanceId> targets);
1619
+ _dirtyTargetSets.TryGetValue(typeIndex, out HashSet<InstanceId> targetSet);
1620
+ int highWaterCount = GetDirtyTargetHighWaterCount(typeIndex);
1621
+ ReturnDirtyTargetList(targets, highWaterCount);
1622
+ ReturnDirtyTargetSet(targetSet, highWaterCount);
1623
+ _dirtyTargetHighWaterCounts.Remove(typeIndex);
1624
+ }
1625
+
1626
+ private void ReturnAllDirtyTargetCollections()
1627
+ {
1628
+ foreach (KeyValuePair<int, List<InstanceId>> entry in _dirtyTargets)
1629
+ {
1630
+ int highWaterCount = GetDirtyTargetHighWaterCount(entry.Key);
1631
+ ReturnDirtyTargetList(entry.Value, highWaterCount);
1632
+ }
1633
+
1634
+ foreach (KeyValuePair<int, HashSet<InstanceId>> entry in _dirtyTargetSets)
1635
+ {
1636
+ int highWaterCount = GetDirtyTargetHighWaterCount(entry.Key);
1637
+ ReturnDirtyTargetSet(entry.Value, highWaterCount);
1638
+ }
1639
+
1640
+ _dirtyTargetHighWaterCounts.Clear();
1641
+ }
1642
+
1643
+ private int GetDirtyTargetHighWaterCount(int typeIndex)
1644
+ {
1645
+ return _dirtyTargetHighWaterCounts.TryGetValue(typeIndex, out int count) ? count : 0;
1646
+ }
1647
+
1648
+ private static void ReturnDirtyTargetList(List<InstanceId> targets, int highWaterCount)
1649
+ {
1650
+ if (targets == null)
1651
+ {
1652
+ return;
1653
+ }
1654
+
1655
+ if (ShouldDropOversizedPoolEntry(highWaterCount, DxPools.InstanceIdLists.MaxRetained))
1656
+ {
1657
+ targets.Clear();
1658
+ return;
1659
+ }
1660
+
1661
+ DxPools.InstanceIdLists.Return(targets);
1662
+ }
1663
+
1664
+ private static void ReturnDirtyTargetSet(HashSet<InstanceId> targets, int highWaterCount)
1665
+ {
1666
+ if (targets == null)
1667
+ {
1668
+ return;
1669
+ }
1670
+
1671
+ if (ShouldDropOversizedPoolEntry(highWaterCount, DxPools.InstanceIdSets.MaxRetained))
1672
+ {
1673
+ targets.Clear();
1674
+ return;
1675
+ }
1676
+
1677
+ DxPools.InstanceIdSets.Return(targets);
1678
+ }
1679
+
1680
+ internal CollectionPoolDiagnostics GetContextDictPoolDiagnosticsForTesting()
1681
+ {
1682
+ return ContextHandlerByTargetDicts.Snapshot();
1683
+ }
1684
+
1685
+ private Dictionary<InstanceId, HandlerCache<int, HandlerCache>> GetOrRentContextMap<T>(
1686
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks
1687
+ )
1688
+ where T : IMessage
1689
+ {
1690
+ if (
1691
+ sinks.TryGetValue<T>(
1692
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
1693
+ )
1694
+ )
1695
+ {
1696
+ return handlersByTarget;
1697
+ }
1698
+
1699
+ handlersByTarget = ContextHandlerByTargetDicts.Rent();
1700
+ _contextMapHighWaterCounts[handlersByTarget] = handlersByTarget.Count;
1701
+ sinks.Set<T>(handlersByTarget);
1702
+ return handlersByTarget;
1703
+ }
1704
+
1705
+ private void RemoveAndReturnContextMap(
1706
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sink,
1707
+ int typeIndex,
1708
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
1709
+ )
1710
+ {
1711
+ sink.RemoveAtIndex(typeIndex);
1712
+ ReturnContextMap(handlersByTarget);
1713
+ }
1714
+
1715
+ private void ReturnContextMap(
1716
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
1717
+ )
1718
+ {
1719
+ if (handlersByTarget == null)
1720
+ {
1721
+ return;
1722
+ }
1723
+
1724
+ int highWaterCount = GetContextMapHighWaterCount(handlersByTarget);
1725
+ _contextMapHighWaterCounts.Remove(handlersByTarget);
1726
+
1727
+ foreach (HandlerCache<int, HandlerCache> handlers in handlersByTarget.Values)
1728
+ {
1729
+ handlers?.Clear();
1730
+ }
1731
+
1732
+ handlersByTarget.Clear();
1733
+ if (
1734
+ ShouldDropOversizedPoolEntry(
1735
+ highWaterCount,
1736
+ ContextHandlerByTargetDicts.MaxRetained
1737
+ )
1738
+ )
1739
+ {
1740
+ return;
1741
+ }
1742
+
1743
+ ContextHandlerByTargetDicts.Return(handlersByTarget);
1744
+ }
1745
+
1746
+ private void ClearAndReturnContextSink(
1747
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sink
1748
+ )
1749
+ {
1750
+ foreach (
1751
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget in sink
1752
+ )
1753
+ {
1754
+ ReturnContextMap(handlersByTarget);
1755
+ }
1756
+
1757
+ sink.Clear();
1758
+ }
1759
+
1760
+ private void TrackContextMapHighWater(
1761
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
1762
+ )
1763
+ {
1764
+ if (handlersByTarget == null)
1765
+ {
1766
+ return;
1767
+ }
1768
+
1769
+ _contextMapHighWaterCounts[handlersByTarget] = Math.Max(
1770
+ GetContextMapHighWaterCount(handlersByTarget),
1771
+ handlersByTarget.Count
1772
+ );
1773
+ }
1774
+
1775
+ private int GetContextMapHighWaterCount(
1776
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
1777
+ )
1778
+ {
1779
+ return _contextMapHighWaterCounts.TryGetValue(handlersByTarget, out int count)
1780
+ ? count
1781
+ : handlersByTarget?.Count ?? 0;
1782
+ }
1783
+
1784
+ private static bool ShouldDropOversizedPoolEntry(int retainedEntryCount, int maxRetained)
1785
+ {
1786
+ return maxRetained > 0 && retainedEntryCount > maxRetained;
1787
+ }
1788
+
1789
+ private void ClearDirtyTypeCandidatesWithoutEmptySlots()
1790
+ {
1791
+ int write = 0;
1792
+ for (int i = 0; i < _dirtyTypes.Count; ++i)
1793
+ {
1794
+ int typeIndex = _dirtyTypes[i];
1795
+ if (HasEmptyScalarTypeCandidate(typeIndex))
1796
+ {
1797
+ _dirtyTypes[write++] = typeIndex;
1798
+ continue;
1799
+ }
1800
+
1801
+ _dirtyTypeSet.Remove(typeIndex);
1802
+ }
1803
+
1804
+ if (write < _dirtyTypes.Count)
1805
+ {
1806
+ _dirtyTypes.RemoveRange(write, _dirtyTypes.Count - write);
1807
+ }
1808
+ }
1809
+
1810
+ private bool HasEmptyScalarTypeCandidate(int typeIndex)
1811
+ {
1812
+ for (int sinkIndex = 0; sinkIndex < _scalarSinks.Length; ++sinkIndex)
1813
+ {
1814
+ MessageCache<HandlerCache<int, HandlerCache>> sink = _scalarSinks[sinkIndex];
1815
+ if (
1816
+ sink != null
1817
+ && sink.TryGetValueAtIndex(
1818
+ typeIndex,
1819
+ out HandlerCache<int, HandlerCache> handlers
1820
+ )
1821
+ && handlers.handlers.Count == 0
1822
+ )
1823
+ {
1824
+ return true;
1825
+ }
1826
+ }
1827
+
1828
+ return HasEmptyInterceptorTypeCandidate(_untargetedInterceptsByType, typeIndex)
1829
+ || HasEmptyInterceptorTypeCandidate(_targetedInterceptsByType, typeIndex)
1830
+ || HasEmptyInterceptorTypeCandidate(_broadcastInterceptsByType, typeIndex);
1831
+ }
1832
+
1833
+ private static bool HasEmptyInterceptorTypeCandidate(
1834
+ MessageCache<InterceptorCache<object>> interceptorsByType,
1835
+ int typeIndex
1836
+ )
1837
+ {
1838
+ return interceptorsByType.TryGetValueAtIndex(
1839
+ typeIndex,
1840
+ out InterceptorCache<object> interceptors
1841
+ )
1842
+ && interceptors.handlers.Count == 0;
1843
+ }
1844
+
1845
+ private void ClearDirtyTargetCandidatesWithoutEmptySlots()
1846
+ {
1847
+ List<int> emptyTypeKeys = null;
1848
+ foreach (KeyValuePair<int, List<InstanceId>> entry in _dirtyTargets)
1849
+ {
1850
+ int typeIndex = entry.Key;
1851
+ List<InstanceId> targets = entry.Value;
1852
+ _dirtyTargetSets.TryGetValue(typeIndex, out HashSet<InstanceId> targetSet);
1853
+ int write = 0;
1854
+ for (int i = 0; i < targets.Count; ++i)
1855
+ {
1856
+ InstanceId target = targets[i];
1857
+ if (HasEmptyTargetCandidate(typeIndex, target))
1858
+ {
1859
+ targets[write++] = target;
1860
+ continue;
1861
+ }
1862
+
1863
+ targetSet?.Remove(target);
1864
+ }
1865
+
1866
+ if (write < targets.Count)
1867
+ {
1868
+ targets.RemoveRange(write, targets.Count - write);
1869
+ }
1870
+
1871
+ if (targets.Count == 0)
1872
+ {
1873
+ (emptyTypeKeys ??= new List<int>()).Add(typeIndex);
1874
+ }
1875
+ }
1876
+
1877
+ if (emptyTypeKeys == null)
1878
+ {
1879
+ return;
1880
+ }
1881
+
1882
+ for (int i = 0; i < emptyTypeKeys.Count; ++i)
1883
+ {
1884
+ int typeIndex = emptyTypeKeys[i];
1885
+ ReturnDirtyTargetCollections(typeIndex);
1886
+ _dirtyTargets.Remove(typeIndex);
1887
+ _dirtyTargetSets.Remove(typeIndex);
305
1888
  }
306
1889
  }
307
1890
 
308
- public int RegisteredTargeted
1891
+ private bool HasEmptyTargetCandidate(int typeIndex, InstanceId target)
309
1892
  {
310
- get
1893
+ for (int sinkIndex = 0; sinkIndex < _contextSinks.Length; ++sinkIndex)
311
1894
  {
312
- int count = 0;
313
- foreach (
314
- Dictionary<InstanceId, HandlerCache<int, HandlerCache>> entry in _targetedSinks
1895
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sink =
1896
+ _contextSinks[sinkIndex];
1897
+ if (
1898
+ sink != null
1899
+ && sink.TryGetValueAtIndex(
1900
+ typeIndex,
1901
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
1902
+ )
1903
+ && handlersByTarget.TryGetValue(
1904
+ target,
1905
+ out HandlerCache<int, HandlerCache> handlers
1906
+ )
1907
+ && handlers.handlers.Count == 0
315
1908
  )
316
1909
  {
317
- count += entry?.Count ?? 0;
1910
+ return true;
318
1911
  }
319
-
320
- return count;
321
1912
  }
322
- }
323
1913
 
324
- public int RegisteredGlobalSequentialIndex { get; } = GenerateNewGlobalSequentialIndex();
1914
+ return false;
1915
+ }
325
1916
 
326
- public int RegisteredBroadcast
1917
+ private void ClearDirtyHandlerCandidatesWithoutEmptySlots()
327
1918
  {
328
- get
1919
+ int write = 0;
1920
+ for (int i = 0; i < _dirtyHandlers.Count; ++i)
329
1921
  {
330
- int count = 0;
331
- foreach (
332
- Dictionary<InstanceId, HandlerCache<int, HandlerCache>> entry in _broadcastSinks
1922
+ MessageHandler handler = _dirtyHandlers[i];
1923
+ if (
1924
+ handler != null
1925
+ && _dirtyHandlerSet.Contains(handler)
1926
+ && handler.CountEmptyTypedSlotsForSweep(this) > 0
333
1927
  )
334
1928
  {
335
- count += entry?.Count ?? 0;
1929
+ _dirtyHandlers[write++] = handler;
1930
+ continue;
336
1931
  }
337
1932
 
338
- return count;
1933
+ _dirtyHandlerSet.Remove(handler);
1934
+ _dirtyHandlerTicks.Remove(handler);
1935
+ }
1936
+
1937
+ if (write < _dirtyHandlers.Count)
1938
+ {
1939
+ _dirtyHandlers.RemoveRange(write, _dirtyHandlers.Count - write);
339
1940
  }
340
1941
  }
341
1942
 
342
- public int RegisteredUntargeted
1943
+ private int CountDirtyEmptyTypedHandlerSlots()
343
1944
  {
344
- get
1945
+ int count = 0;
1946
+ for (int i = 0; i < _dirtyHandlers.Count; ++i)
345
1947
  {
346
- int count = 0;
347
- foreach (HandlerCache<int, HandlerCache> entry in _sinks)
1948
+ MessageHandler handler = _dirtyHandlers[i];
1949
+ if (handler != null && _dirtyHandlerSet.Contains(handler))
348
1950
  {
349
- count += entry?.handlers?.Count ?? 0;
1951
+ count += handler.CountEmptyTypedSlotsForSweep(this);
350
1952
  }
351
-
352
- return count;
353
1953
  }
354
- }
355
1954
 
356
- public bool DiagnosticsMode
357
- {
358
- get => _diagnosticsMode;
359
- set => _diagnosticsMode = value;
1955
+ return count;
360
1956
  }
361
1957
 
362
- private static readonly Type MessageBusType = typeof(MessageBus);
363
-
364
- // For use with re-broadcasting to generic methods
365
- private static readonly object[] ReflectionMethodArgumentsCache = new object[2];
366
- private static readonly List<Expression> ArgumentExpressionsCache = new();
367
-
368
- private const BindingFlags ReflectionHelperBindingFlags =
369
- BindingFlags.Static | BindingFlags.NonPublic;
370
- private const BindingFlags ReflexiveMethodBindingFlags =
371
- BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
372
-
373
- private delegate void FastUntargetedBroadcast<T>(ref T message)
374
- where T : IUntargetedMessage;
375
- private delegate void FastTargetedBroadcast<T>(ref InstanceId target, ref T message)
376
- where T : ITargetedMessage;
377
- private delegate void FastSourcedBroadcast<T>(ref InstanceId target, ref T message)
378
- where T : IBroadcastMessage;
379
-
380
- public RegistrationLog Log => _log;
381
-
382
- private readonly MessageCache<HandlerCache<int, HandlerCache>> _sinks = new();
383
- private readonly MessageCache<
384
- Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
385
- > _targetedSinks = new();
386
- private readonly MessageCache<
387
- Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
388
- > _broadcastSinks = new();
389
- private readonly MessageCache<HandlerCache<int, HandlerCache>> _postProcessingSinks = new();
390
- private readonly MessageCache<
391
- Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
392
- > _postProcessingTargetedSinks = new();
393
- private readonly MessageCache<
394
- Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
395
- > _postProcessingBroadcastSinks = new();
396
- private readonly MessageCache<
397
- HandlerCache<int, HandlerCache>
398
- > _postProcessingTargetedWithoutTargetingSinks = new();
399
- private readonly MessageCache<
400
- HandlerCache<int, HandlerCache>
401
- > _postProcessingBroadcastWithoutSourceSinks = new();
402
- private readonly HandlerCache _globalSinks = new();
403
-
404
- // Interceptors split by category to avoid mixing types
405
- private readonly MessageCache<InterceptorCache<object>> _untargetedInterceptsByType = new();
406
- private readonly MessageCache<InterceptorCache<object>> _targetedInterceptsByType = new();
407
- private readonly MessageCache<InterceptorCache<object>> _broadcastInterceptsByType = new();
408
- private readonly Dictionary<object, Dictionary<int, int>> _uniqueInterceptorsAndPriorities =
409
- new();
410
-
411
- private readonly Dictionary<Type, object> _broadcastMethodsByType = new();
412
- private readonly Stack<List<object>> _innerInterceptorsStack = new();
413
-
414
- private readonly Dictionary<
415
- Type,
416
- Dictionary<MethodSignatureKey, Action<MonoBehaviour, object[]>>
417
- > _methodCache = new();
418
-
419
- #if UNITY_2021_3_OR_NEWER
420
- private readonly HashSet<MonoBehaviour> _recipientCache = new();
421
- private readonly List<MonoBehaviour> _componentCache = new();
422
- #endif
423
-
424
- private readonly RegistrationLog _log = new();
425
- internal readonly CyclicBuffer<MessageEmissionData> _emissionBuffer = new(
426
- GlobalMessageBufferSize
427
- );
1958
+ private static int CountOccupiedInterceptorTypeSlots(
1959
+ MessageCache<InterceptorCache<object>> cache
1960
+ )
1961
+ {
1962
+ int count = 0;
1963
+ foreach (InterceptorCache<object> entry in cache)
1964
+ {
1965
+ if (entry != null)
1966
+ {
1967
+ count++;
1968
+ }
1969
+ }
428
1970
 
429
- private bool _diagnosticsMode = ShouldEnableDiagnostics();
430
- private bool _loggedReflexiveWarning;
1971
+ return count;
1972
+ }
431
1973
 
432
1974
  internal void ResetState()
433
1975
  {
1976
+ ResetTypedSlotsForReferencedHandlers();
434
1977
  _emissionId = 0;
1978
+ _tickCounter = 0;
435
1979
  _diagnosticsMode = ShouldEnableDiagnostics();
436
1980
  _loggedReflexiveWarning = false;
437
-
438
- _sinks.Clear();
439
- _targetedSinks.Clear();
440
- _broadcastSinks.Clear();
441
- _postProcessingSinks.Clear();
442
- _postProcessingTargetedSinks.Clear();
443
- _postProcessingBroadcastSinks.Clear();
444
- _postProcessingTargetedWithoutTargetingSinks.Clear();
445
- _postProcessingBroadcastWithoutSourceSinks.Clear();
446
- _globalSinks.Clear();
1981
+ BumpResetGeneration();
1982
+
1983
+ _scalarSinks[BusSinkIndex.UntargetedHandleDefault].Clear();
1984
+ _scalarSinks[BusSinkIndex.BroadcastHandleWithoutContext].Clear();
1985
+ _scalarSinks[BusSinkIndex.TargetedHandleWithoutContext].Clear();
1986
+ ClearAndReturnContextSink(_contextSinks[BusContextIndex.TargetedHandleDefault]);
1987
+ ClearAndReturnContextSink(_contextSinks[BusContextIndex.BroadcastHandleDefault]);
1988
+ _scalarSinks[BusSinkIndex.UntargetedPostProcessDefault].Clear();
1989
+ ClearAndReturnContextSink(_contextSinks[BusContextIndex.TargetedPostProcessDefault]);
1990
+ ClearAndReturnContextSink(_contextSinks[BusContextIndex.BroadcastPostProcessDefault]);
1991
+ _scalarSinks[BusSinkIndex.TargetedPostProcessWithoutContext].Clear();
1992
+ _scalarSinks[BusSinkIndex.BroadcastPostProcessWithoutContext].Clear();
1993
+ _globalSlots.Clear();
447
1994
 
448
1995
  _untargetedInterceptsByType.Clear();
449
1996
  _targetedInterceptsByType.Clear();
@@ -452,6 +1999,18 @@ namespace DxMessaging.Core.MessageBus
452
1999
  _broadcastMethodsByType.Clear();
453
2000
  _innerInterceptorsStack.Clear();
454
2001
  _methodCache.Clear();
2002
+ _dirtyTypes.Clear();
2003
+ ReturnAllDirtyTargetCollections();
2004
+ _dirtyTargets.Clear();
2005
+ _dirtyTypeSet.Clear();
2006
+ _dirtyTargetSets.Clear();
2007
+ _dirtyTargetHighWaterCounts.Clear();
2008
+ _contextMapHighWaterCounts.Clear();
2009
+ _dirtyHandlers.Clear();
2010
+ _dirtyHandlerSet.Clear();
2011
+ _dirtyHandlerTicks.Clear();
2012
+ _globalSlotSweepCandidate = false;
2013
+ _lastSweepSeconds = _clock.NowSeconds;
455
2014
 
456
2015
  #if UNITY_2021_3_OR_NEWER
457
2016
  _recipientCache.Clear();
@@ -465,13 +2024,93 @@ namespace DxMessaging.Core.MessageBus
465
2024
  _emissionBuffer.Clear();
466
2025
  }
467
2026
 
2027
+ private void ResetTypedSlotsForReferencedHandlers()
2028
+ {
2029
+ HashSet<MessageHandler> handlers = new HashSet<MessageHandler>();
2030
+ AddHandlersFromScalarSinks(handlers);
2031
+ AddHandlersFromContextSinks(handlers);
2032
+
2033
+ foreach (MessageHandler handler in _globalSlots.sharedHandlers.Keys)
2034
+ {
2035
+ handlers.Add(handler);
2036
+ }
2037
+
2038
+ foreach (MessageHandler handler in handlers)
2039
+ {
2040
+ handler.ResetAllTypedSlotsForBusReset(this);
2041
+ }
2042
+ }
2043
+
2044
+ private void AddHandlersFromScalarSinks(HashSet<MessageHandler> handlers)
2045
+ {
2046
+ foreach (MessageCache<HandlerCache<int, HandlerCache>> sink in _scalarSinks)
2047
+ {
2048
+ if (sink == null)
2049
+ {
2050
+ continue;
2051
+ }
2052
+
2053
+ foreach (HandlerCache<int, HandlerCache> handlersByPriority in sink)
2054
+ {
2055
+ AddHandlersFromPriorityCache(handlersByPriority, handlers);
2056
+ }
2057
+ }
2058
+ }
2059
+
2060
+ private void AddHandlersFromContextSinks(HashSet<MessageHandler> handlers)
2061
+ {
2062
+ foreach (
2063
+ MessageCache<
2064
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
2065
+ > sink in _contextSinks
2066
+ )
2067
+ {
2068
+ foreach (
2069
+ Dictionary<
2070
+ InstanceId,
2071
+ HandlerCache<int, HandlerCache>
2072
+ > handlersByContext in sink
2073
+ )
2074
+ {
2075
+ foreach (
2076
+ HandlerCache<
2077
+ int,
2078
+ HandlerCache
2079
+ > handlersByPriority in handlersByContext.Values
2080
+ )
2081
+ {
2082
+ AddHandlersFromPriorityCache(handlersByPriority, handlers);
2083
+ }
2084
+ }
2085
+ }
2086
+ }
2087
+
2088
+ private static void AddHandlersFromPriorityCache(
2089
+ HandlerCache<int, HandlerCache> handlersByPriority,
2090
+ HashSet<MessageHandler> handlers
2091
+ )
2092
+ {
2093
+ if (handlersByPriority == null)
2094
+ {
2095
+ return;
2096
+ }
2097
+
2098
+ foreach (HandlerCache cache in handlersByPriority.handlers.Values)
2099
+ {
2100
+ foreach (MessageHandler handler in cache.handlers.Keys)
2101
+ {
2102
+ handlers.Add(handler);
2103
+ }
2104
+ }
2105
+ }
2106
+
468
2107
  /// <inheritdoc />
469
2108
  public Action RegisterUntargeted<T>(MessageHandler messageHandler, int priority = 0)
470
2109
  where T : IUntargetedMessage
471
2110
  {
472
2111
  return InternalRegisterUntargeted<T>(
473
2112
  messageHandler,
474
- _sinks,
2113
+ _scalarSinks[BusSinkIndex.UntargetedHandleDefault],
475
2114
  RegistrationMethod.Untargeted,
476
2115
  priority
477
2116
  );
@@ -488,7 +2127,7 @@ namespace DxMessaging.Core.MessageBus
488
2127
  return InternalRegisterWithContext<T>(
489
2128
  target,
490
2129
  messageHandler,
491
- _targetedSinks,
2130
+ _contextSinks[BusContextIndex.TargetedHandleDefault],
492
2131
  RegistrationMethod.Targeted,
493
2132
  priority
494
2133
  );
@@ -505,7 +2144,7 @@ namespace DxMessaging.Core.MessageBus
505
2144
  return InternalRegisterWithContext<T>(
506
2145
  source,
507
2146
  messageHandler,
508
- _broadcastSinks,
2147
+ _contextSinks[BusContextIndex.BroadcastHandleDefault],
509
2148
  RegistrationMethod.Broadcast,
510
2149
  priority
511
2150
  );
@@ -520,7 +2159,7 @@ namespace DxMessaging.Core.MessageBus
520
2159
  {
521
2160
  return InternalRegisterUntargeted<T>(
522
2161
  messageHandler,
523
- _sinks,
2162
+ _scalarSinks[BusSinkIndex.BroadcastHandleWithoutContext],
524
2163
  RegistrationMethod.BroadcastWithoutSource,
525
2164
  priority
526
2165
  );
@@ -535,7 +2174,7 @@ namespace DxMessaging.Core.MessageBus
535
2174
  {
536
2175
  return InternalRegisterUntargeted<T>(
537
2176
  messageHandler,
538
- _sinks,
2177
+ _scalarSinks[BusSinkIndex.TargetedHandleWithoutContext],
539
2178
  RegistrationMethod.TargetedWithoutTargeting,
540
2179
  priority
541
2180
  );
@@ -544,11 +2183,21 @@ namespace DxMessaging.Core.MessageBus
544
2183
  /// <inheritdoc />
545
2184
  public Action RegisterGlobalAcceptAll(MessageHandler messageHandler)
546
2185
  {
547
- _globalSinks.version++;
548
- int count = _globalSinks.handlers.GetValueOrDefault(messageHandler, 0);
2186
+ long touchTick = AdvanceTick();
2187
+ _globalSlots.lastTouchTicks = touchTick;
2188
+ _globalSlots.version++;
2189
+ int count = _globalSlots.sharedHandlers.GetValueOrDefault(messageHandler, 0);
549
2190
 
550
2191
  Type type = typeof(IMessage);
551
- _globalSinks.handlers[messageHandler] = count + 1;
2192
+ _globalSlots.sharedHandlers[messageHandler] = count + 1;
2193
+ // liveCount mirrors sharedHandlers.Count at every stable
2194
+ // observation point; only newly-inserted handlers (the 0 -> 1
2195
+ // transition in the per-handler refcount) advance it. See
2196
+ // BusGlobalSlot.liveCount xmldoc for the full invariant.
2197
+ if (count == 0)
2198
+ {
2199
+ _globalSlots.liveCount++;
2200
+ }
552
2201
  _log.Log(
553
2202
  new MessagingRegistration(
554
2203
  messageHandler.owner,
@@ -560,23 +2209,37 @@ namespace DxMessaging.Core.MessageBus
560
2209
 
561
2210
  StageGlobalDispatchSnapshot<IUntargetedMessage>(
562
2211
  this,
563
- _globalSinks,
564
- DispatchCategory.GlobalUntargeted
2212
+ _globalSlots,
2213
+ DispatchKind.Untargeted
565
2214
  );
566
2215
  StageGlobalDispatchSnapshot<ITargetedMessage>(
567
2216
  this,
568
- _globalSinks,
569
- DispatchCategory.GlobalTargeted
2217
+ _globalSlots,
2218
+ DispatchKind.Targeted
570
2219
  );
571
2220
  StageGlobalDispatchSnapshot<IBroadcastMessage>(
572
2221
  this,
573
- _globalSinks,
574
- DispatchCategory.GlobalBroadcast
2222
+ _globalSlots,
2223
+ DispatchKind.Broadcast
575
2224
  );
2225
+ DebugAssertGlobalLiveCount();
576
2226
 
2227
+ long capturedGeneration = _resetGeneration;
2228
+ long capturedSweepGeneration = _globalSlotSweepGeneration;
577
2229
  return () =>
578
2230
  {
579
- _globalSinks.version++;
2231
+ // Generation guard: see InternalRegisterUntargeted for the
2232
+ // rationale. Skip silently when the closure outlived a Reset.
2233
+ if (
2234
+ capturedGeneration != _resetGeneration
2235
+ || capturedSweepGeneration != _globalSlotSweepGeneration
2236
+ )
2237
+ {
2238
+ return;
2239
+ }
2240
+
2241
+ long deregisterTouchTick = AdvanceTick();
2242
+ _globalSlots.version++;
580
2243
  _log.Log(
581
2244
  new MessagingRegistration(
582
2245
  messageHandler.owner,
@@ -585,7 +2248,7 @@ namespace DxMessaging.Core.MessageBus
585
2248
  RegistrationMethod.GlobalAcceptAll
586
2249
  )
587
2250
  );
588
- if (!_globalSinks.handlers.TryGetValue(messageHandler, out count))
2251
+ if (!_globalSlots.sharedHandlers.TryGetValue(messageHandler, out count))
589
2252
  {
590
2253
  if (MessagingDebug.enabled)
591
2254
  {
@@ -599,30 +2262,39 @@ namespace DxMessaging.Core.MessageBus
599
2262
  return;
600
2263
  }
601
2264
 
2265
+ _globalSlots.lastTouchTicks = deregisterTouchTick;
602
2266
  if (count <= 1)
603
2267
  {
604
- _ = _globalSinks.handlers.Remove(messageHandler);
2268
+ _ = _globalSlots.sharedHandlers.Remove(messageHandler);
2269
+ MarkDirtyHandler(messageHandler);
2270
+ _globalSlotSweepCandidate = true;
2271
+ // Final-removal of this handler from sharedHandlers is the
2272
+ // 1 -> 0 transition that mirrors back into liveCount.
2273
+ // Partial deregistration (count > 1) leaves liveCount
2274
+ // alone -- the dictionary entry is still present.
2275
+ _globalSlots.liveCount--;
605
2276
  }
606
2277
  else
607
2278
  {
608
- _globalSinks.handlers[messageHandler] = count - 1;
2279
+ _globalSlots.sharedHandlers[messageHandler] = count - 1;
609
2280
  }
610
2281
 
611
2282
  StageGlobalDispatchSnapshot<IUntargetedMessage>(
612
2283
  this,
613
- _globalSinks,
614
- DispatchCategory.GlobalUntargeted
2284
+ _globalSlots,
2285
+ DispatchKind.Untargeted
615
2286
  );
616
2287
  StageGlobalDispatchSnapshot<ITargetedMessage>(
617
2288
  this,
618
- _globalSinks,
619
- DispatchCategory.GlobalTargeted
2289
+ _globalSlots,
2290
+ DispatchKind.Targeted
620
2291
  );
621
2292
  StageGlobalDispatchSnapshot<IBroadcastMessage>(
622
2293
  this,
623
- _globalSinks,
624
- DispatchCategory.GlobalBroadcast
2294
+ _globalSlots,
2295
+ DispatchKind.Broadcast
625
2296
  );
2297
+ DebugAssertGlobalLiveCount();
626
2298
  };
627
2299
  }
628
2300
 
@@ -633,8 +2305,12 @@ namespace DxMessaging.Core.MessageBus
633
2305
  )
634
2306
  where T : IUntargetedMessage
635
2307
  {
2308
+ _ = AdvanceTick();
636
2309
  InterceptorCache<object> prioritizedInterceptors =
637
2310
  _untargetedInterceptsByType.GetOrAdd<T>();
2311
+ InterceptorCache<object> capturedInterceptors = prioritizedInterceptors;
2312
+ prioritizedInterceptors.lastTouchTicks = _tickCounter;
2313
+ MarkDirtyType<T>();
638
2314
 
639
2315
  if (
640
2316
  !_uniqueInterceptorsAndPriorities.TryGetValue(
@@ -676,8 +2352,28 @@ namespace DxMessaging.Core.MessageBus
676
2352
  )
677
2353
  );
678
2354
 
2355
+ long capturedGeneration = _resetGeneration;
679
2356
  return () =>
680
2357
  {
2358
+ // Generation guard: see InternalRegisterUntargeted for the
2359
+ // rationale. Skip silently when the closure outlived a Reset.
2360
+ if (capturedGeneration != _resetGeneration)
2361
+ {
2362
+ return;
2363
+ }
2364
+ if (
2365
+ IsStaleInterceptorDeregisterAfterSweep<T>(
2366
+ _untargetedInterceptsByType,
2367
+ capturedInterceptors
2368
+ )
2369
+ )
2370
+ {
2371
+ return;
2372
+ }
2373
+
2374
+ _ = AdvanceTick();
2375
+ prioritizedInterceptors.lastTouchTicks = _tickCounter;
2376
+ MarkDirtyType<T>();
681
2377
  _log.Log(
682
2378
  new MessagingRegistration(
683
2379
  InstanceId.EmptyId,
@@ -755,8 +2451,12 @@ namespace DxMessaging.Core.MessageBus
755
2451
  )
756
2452
  where T : ITargetedMessage
757
2453
  {
2454
+ _ = AdvanceTick();
758
2455
  InterceptorCache<object> prioritizedInterceptors =
759
2456
  _targetedInterceptsByType.GetOrAdd<T>();
2457
+ InterceptorCache<object> capturedInterceptors = prioritizedInterceptors;
2458
+ prioritizedInterceptors.lastTouchTicks = _tickCounter;
2459
+ MarkDirtyType<T>();
760
2460
 
761
2461
  if (
762
2462
  !_uniqueInterceptorsAndPriorities.TryGetValue(
@@ -798,8 +2498,28 @@ namespace DxMessaging.Core.MessageBus
798
2498
  )
799
2499
  );
800
2500
 
2501
+ long capturedGeneration = _resetGeneration;
801
2502
  return () =>
802
2503
  {
2504
+ // Generation guard: see InternalRegisterUntargeted for the
2505
+ // rationale. Skip silently when the closure outlived a Reset.
2506
+ if (capturedGeneration != _resetGeneration)
2507
+ {
2508
+ return;
2509
+ }
2510
+ if (
2511
+ IsStaleInterceptorDeregisterAfterSweep<T>(
2512
+ _targetedInterceptsByType,
2513
+ capturedInterceptors
2514
+ )
2515
+ )
2516
+ {
2517
+ return;
2518
+ }
2519
+
2520
+ _ = AdvanceTick();
2521
+ prioritizedInterceptors.lastTouchTicks = _tickCounter;
2522
+ MarkDirtyType<T>();
803
2523
  _log.Log(
804
2524
  new MessagingRegistration(
805
2525
  InstanceId.EmptyId,
@@ -877,8 +2597,12 @@ namespace DxMessaging.Core.MessageBus
877
2597
  )
878
2598
  where T : IBroadcastMessage
879
2599
  {
2600
+ _ = AdvanceTick();
880
2601
  InterceptorCache<object> prioritizedInterceptors =
881
2602
  _broadcastInterceptsByType.GetOrAdd<T>();
2603
+ InterceptorCache<object> capturedInterceptors = prioritizedInterceptors;
2604
+ prioritizedInterceptors.lastTouchTicks = _tickCounter;
2605
+ MarkDirtyType<T>();
882
2606
 
883
2607
  if (
884
2608
  !_uniqueInterceptorsAndPriorities.TryGetValue(
@@ -920,8 +2644,28 @@ namespace DxMessaging.Core.MessageBus
920
2644
  )
921
2645
  );
922
2646
 
2647
+ long capturedGeneration = _resetGeneration;
923
2648
  return () =>
924
2649
  {
2650
+ // Generation guard: see InternalRegisterUntargeted for the
2651
+ // rationale. Skip silently when the closure outlived a Reset.
2652
+ if (capturedGeneration != _resetGeneration)
2653
+ {
2654
+ return;
2655
+ }
2656
+ if (
2657
+ IsStaleInterceptorDeregisterAfterSweep<T>(
2658
+ _broadcastInterceptsByType,
2659
+ capturedInterceptors
2660
+ )
2661
+ )
2662
+ {
2663
+ return;
2664
+ }
2665
+
2666
+ _ = AdvanceTick();
2667
+ prioritizedInterceptors.lastTouchTicks = _tickCounter;
2668
+ MarkDirtyType<T>();
925
2669
  _log.Log(
926
2670
  new MessagingRegistration(
927
2671
  InstanceId.EmptyId,
@@ -992,6 +2736,17 @@ namespace DxMessaging.Core.MessageBus
992
2736
  };
993
2737
  }
994
2738
 
2739
+ private bool IsStaleInterceptorDeregisterAfterSweep<T>(
2740
+ MessageCache<InterceptorCache<object>> interceptorsByType,
2741
+ InterceptorCache<object> capturedInterceptors
2742
+ )
2743
+ where T : IMessage
2744
+ {
2745
+ return !interceptorsByType.TryGetValue<T>(
2746
+ out InterceptorCache<object> currentInterceptors
2747
+ ) || !ReferenceEquals(currentInterceptors, capturedInterceptors);
2748
+ }
2749
+
995
2750
  /// <inheritdoc />
996
2751
  public Action RegisterUntargetedPostProcessor<T>(
997
2752
  MessageHandler messageHandler,
@@ -1001,7 +2756,7 @@ namespace DxMessaging.Core.MessageBus
1001
2756
  {
1002
2757
  return InternalRegisterUntargeted<T>(
1003
2758
  messageHandler,
1004
- _postProcessingSinks,
2759
+ _scalarSinks[BusSinkIndex.UntargetedPostProcessDefault],
1005
2760
  RegistrationMethod.UntargetedPostProcessor,
1006
2761
  priority
1007
2762
  );
@@ -1018,7 +2773,7 @@ namespace DxMessaging.Core.MessageBus
1018
2773
  return InternalRegisterWithContext<T>(
1019
2774
  target,
1020
2775
  messageHandler,
1021
- _postProcessingTargetedSinks,
2776
+ _contextSinks[BusContextIndex.TargetedPostProcessDefault],
1022
2777
  RegistrationMethod.TargetedPostProcessor,
1023
2778
  priority
1024
2779
  );
@@ -1033,7 +2788,7 @@ namespace DxMessaging.Core.MessageBus
1033
2788
  {
1034
2789
  return InternalRegisterUntargeted<T>(
1035
2790
  messageHandler,
1036
- _postProcessingTargetedWithoutTargetingSinks,
2791
+ _scalarSinks[BusSinkIndex.TargetedPostProcessWithoutContext],
1037
2792
  RegistrationMethod.TargetedWithoutTargetingPostProcessor,
1038
2793
  priority
1039
2794
  );
@@ -1050,7 +2805,7 @@ namespace DxMessaging.Core.MessageBus
1050
2805
  return InternalRegisterWithContext<T>(
1051
2806
  source,
1052
2807
  messageHandler,
1053
- _postProcessingBroadcastSinks,
2808
+ _contextSinks[BusContextIndex.BroadcastPostProcessDefault],
1054
2809
  RegistrationMethod.BroadcastPostProcessor,
1055
2810
  priority
1056
2811
  );
@@ -1065,7 +2820,7 @@ namespace DxMessaging.Core.MessageBus
1065
2820
  {
1066
2821
  return InternalRegisterUntargeted<T>(
1067
2822
  messageHandler,
1068
- _postProcessingBroadcastWithoutSourceSinks,
2823
+ _scalarSinks[BusSinkIndex.BroadcastPostProcessWithoutContext],
1069
2824
  RegistrationMethod.BroadcastWithoutSourcePostProcessor,
1070
2825
  priority
1071
2826
  );
@@ -1107,10 +2862,13 @@ namespace DxMessaging.Core.MessageBus
1107
2862
  public void UntargetedBroadcast<TMessage>(ref TMessage typedMessage)
1108
2863
  where TMessage : IUntargetedMessage
1109
2864
  {
2865
+ TrySweepIdle();
2866
+ using DispatchLease dispatchLease = EnterDispatch();
1110
2867
  unchecked
1111
2868
  {
1112
2869
  _emissionId++;
1113
2870
  }
2871
+ long touchTick = AdvanceTick();
1114
2872
  if (_diagnosticsMode)
1115
2873
  {
1116
2874
  _emissionBuffer.Add(new MessageEmissionData(typedMessage));
@@ -1120,16 +2878,18 @@ namespace DxMessaging.Core.MessageBus
1120
2878
  // handlers/post-processors are not observed until the next emission.
1121
2879
  DispatchSnapshot untargetedPostSnapshot = DispatchSnapshot.Empty;
1122
2880
  if (
1123
- _postProcessingSinks.TryGetValue<TMessage>(
1124
- out HandlerCache<int, HandlerCache> untargetedPostHandlers
1125
- )
2881
+ _scalarSinks[BusSinkIndex.UntargetedPostProcessDefault]
2882
+ .TryGetValue<TMessage>(
2883
+ out HandlerCache<int, HandlerCache> untargetedPostHandlers
2884
+ )
1126
2885
  && untargetedPostHandlers.handlers.Count > 0
1127
2886
  )
1128
2887
  {
2888
+ Touch(untargetedPostHandlers, touchTick);
1129
2889
  untargetedPostSnapshot = AcquireDispatchSnapshot<TMessage>(
1130
2890
  this,
1131
2891
  untargetedPostHandlers,
1132
- DispatchCategory.UntargetedPost,
2892
+ UntargetedPostSlot,
1133
2893
  _emissionId
1134
2894
  );
1135
2895
  PrefreezeUntargetedPostSnapshot<TMessage>(untargetedPostSnapshot);
@@ -1140,7 +2900,7 @@ namespace DxMessaging.Core.MessageBus
1140
2900
  return;
1141
2901
  }
1142
2902
 
1143
- if (0 < _globalSinks.handlers.Count)
2903
+ if (0 < _globalSlots.sharedHandlers.Count)
1144
2904
  {
1145
2905
  IUntargetedMessage untargetedMessage = typedMessage;
1146
2906
  BroadcastGlobalUntargeted(ref untargetedMessage);
@@ -1149,17 +2909,17 @@ namespace DxMessaging.Core.MessageBus
1149
2909
  bool foundAnyHandlers = InternalUntargetedBroadcast(ref typedMessage);
1150
2910
 
1151
2911
  if (
1152
- _postProcessingSinks.TryGetValue<TMessage>(
1153
- out HandlerCache<int, HandlerCache> sortedHandlers
1154
- )
2912
+ _scalarSinks[BusSinkIndex.UntargetedPostProcessDefault]
2913
+ .TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> sortedHandlers)
1155
2914
  && 0 < sortedHandlers.handlers.Count
1156
2915
  )
1157
2916
  {
2917
+ Touch(sortedHandlers, touchTick);
1158
2918
  DispatchSnapshot snapshot = untargetedPostSnapshot.IsEmpty
1159
2919
  ? AcquireDispatchSnapshot<TMessage>(
1160
2920
  this,
1161
2921
  sortedHandlers,
1162
- DispatchCategory.UntargetedPost,
2922
+ UntargetedPostSlot,
1163
2923
  _emissionId
1164
2924
  )
1165
2925
  : untargetedPostSnapshot;
@@ -1267,10 +3027,13 @@ namespace DxMessaging.Core.MessageBus
1267
3027
  public void TargetedBroadcast<TMessage>(ref InstanceId target, ref TMessage typedMessage)
1268
3028
  where TMessage : ITargetedMessage
1269
3029
  {
3030
+ TrySweepIdle();
3031
+ using DispatchLease dispatchLease = EnterDispatch();
1270
3032
  unchecked
1271
3033
  {
1272
3034
  _emissionId++;
1273
3035
  }
3036
+ long touchTick = AdvanceTick();
1274
3037
  if (_diagnosticsMode)
1275
3038
  {
1276
3039
  _emissionBuffer.Add(new MessageEmissionData(typedMessage, target));
@@ -1280,9 +3043,13 @@ namespace DxMessaging.Core.MessageBus
1280
3043
  DispatchSnapshot targetedPostSnapshot = DispatchSnapshot.Empty;
1281
3044
  DispatchSnapshot targetedWithoutTargetingPostSnapshot = DispatchSnapshot.Empty;
1282
3045
  if (
1283
- _postProcessingTargetedSinks.TryGetValue<TMessage>(
1284
- out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> targetedPostHandlers
1285
- )
3046
+ _contextSinks[BusContextIndex.TargetedPostProcessDefault]
3047
+ .TryGetValue<TMessage>(
3048
+ out Dictionary<
3049
+ InstanceId,
3050
+ HandlerCache<int, HandlerCache>
3051
+ > targetedPostHandlers
3052
+ )
1286
3053
  && targetedPostHandlers.TryGetValue(
1287
3054
  target,
1288
3055
  out HandlerCache<int, HandlerCache> targetedPostByPriority
@@ -1290,25 +3057,28 @@ namespace DxMessaging.Core.MessageBus
1290
3057
  && targetedPostByPriority.handlers.Count > 0
1291
3058
  )
1292
3059
  {
3060
+ Touch(targetedPostByPriority, touchTick);
1293
3061
  targetedPostSnapshot = AcquireDispatchSnapshot<TMessage>(
1294
3062
  this,
1295
3063
  targetedPostByPriority,
1296
- DispatchCategory.TargetedPost,
3064
+ TargetedPostSlot,
1297
3065
  _emissionId
1298
3066
  );
1299
3067
  PrefreezeTargetedPostSnapshot<TMessage>(ref target, targetedPostSnapshot);
1300
3068
  }
1301
3069
  if (
1302
- _postProcessingTargetedWithoutTargetingSinks.TryGetValue<TMessage>(
1303
- out HandlerCache<int, HandlerCache> targetedWithoutTargetingHandlers
1304
- )
3070
+ _scalarSinks[BusSinkIndex.TargetedPostProcessWithoutContext]
3071
+ .TryGetValue<TMessage>(
3072
+ out HandlerCache<int, HandlerCache> targetedWithoutTargetingHandlers
3073
+ )
1305
3074
  && targetedWithoutTargetingHandlers.handlers.Count > 0
1306
3075
  )
1307
3076
  {
3077
+ Touch(targetedWithoutTargetingHandlers, touchTick);
1308
3078
  targetedWithoutTargetingPostSnapshot = AcquireDispatchSnapshot<TMessage>(
1309
3079
  this,
1310
3080
  targetedWithoutTargetingHandlers,
1311
- DispatchCategory.TargetedWithoutTargetingPost,
3081
+ TargetedWithoutContextPostSlot,
1312
3082
  _emissionId
1313
3083
  );
1314
3084
  PrefreezeTargetedWithoutTargetingPostSnapshot<TMessage>(
@@ -1321,7 +3091,7 @@ namespace DxMessaging.Core.MessageBus
1321
3091
  return;
1322
3092
  }
1323
3093
 
1324
- if (0 < _globalSinks.handlers.Count)
3094
+ if (0 < _globalSlots.sharedHandlers.Count)
1325
3095
  {
1326
3096
  ITargetedMessage targetedMessage = typedMessage;
1327
3097
  BroadcastGlobalTargeted(ref target, ref targetedMessage);
@@ -1538,9 +3308,10 @@ namespace DxMessaging.Core.MessageBus
1538
3308
  }
1539
3309
 
1540
3310
  if (
1541
- _targetedSinks.TryGetValue<TMessage>(
1542
- out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> targetedHandlers
1543
- )
3311
+ _contextSinks[BusContextIndex.TargetedHandleDefault]
3312
+ .TryGetValue<TMessage>(
3313
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> targetedHandlers
3314
+ )
1544
3315
  && targetedHandlers.TryGetValue(
1545
3316
  target,
1546
3317
  out HandlerCache<int, HandlerCache> sortedHandlers
@@ -1548,12 +3319,17 @@ namespace DxMessaging.Core.MessageBus
1548
3319
  && sortedHandlers.handlers.Count > 0
1549
3320
  )
1550
3321
  {
3322
+ Touch(sortedHandlers, touchTick);
1551
3323
  DispatchSnapshot snapshot = AcquireDispatchSnapshot<TMessage>(
1552
3324
  this,
1553
3325
  sortedHandlers,
1554
- DispatchCategory.Targeted,
3326
+ TargetedHandleSlot,
1555
3327
  _emissionId
1556
3328
  );
3329
+ // Pre-freeze the typed-handler caches across every priority bucket so
3330
+ // deregistrations performed by an earlier priority's handler cannot
3331
+ // empty a later priority's stack mid-emission.
3332
+ PrefreezeTargetedSnapshot<TMessage>(ref target, snapshot);
1557
3333
  DispatchBucket[] buckets = snapshot.buckets;
1558
3334
  int bucketCount = snapshot.bucketCount;
1559
3335
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
@@ -1625,7 +3401,8 @@ namespace DxMessaging.Core.MessageBus
1625
3401
  }
1626
3402
 
1627
3403
  if (
1628
- _postProcessingTargetedSinks.TryGetValue<TMessage>(out targetedHandlers)
3404
+ _contextSinks[BusContextIndex.TargetedPostProcessDefault]
3405
+ .TryGetValue<TMessage>(out targetedHandlers)
1629
3406
  && targetedHandlers.TryGetValue(target, out sortedHandlers)
1630
3407
  && sortedHandlers.handlers.Count > 0
1631
3408
  )
@@ -1634,7 +3411,7 @@ namespace DxMessaging.Core.MessageBus
1634
3411
  ? AcquireDispatchSnapshot<TMessage>(
1635
3412
  this,
1636
3413
  sortedHandlers,
1637
- DispatchCategory.TargetedPost,
3414
+ TargetedPostSlot,
1638
3415
  _emissionId
1639
3416
  )
1640
3417
  : targetedPostSnapshot;
@@ -1779,9 +3556,8 @@ namespace DxMessaging.Core.MessageBus
1779
3556
  }
1780
3557
 
1781
3558
  if (
1782
- _postProcessingTargetedWithoutTargetingSinks.TryGetValue<TMessage>(
1783
- out HandlerCache<int, HandlerCache> postTwt
1784
- )
3559
+ _scalarSinks[BusSinkIndex.TargetedPostProcessWithoutContext]
3560
+ .TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> postTwt)
1785
3561
  && postTwt.handlers.Count > 0
1786
3562
  )
1787
3563
  {
@@ -1789,7 +3565,7 @@ namespace DxMessaging.Core.MessageBus
1789
3565
  ? AcquireDispatchSnapshot<TMessage>(
1790
3566
  this,
1791
3567
  postTwt,
1792
- DispatchCategory.TargetedWithoutTargetingPost,
3568
+ TargetedWithoutContextPostSlot,
1793
3569
  _emissionId
1794
3570
  )
1795
3571
  : targetedWithoutTargetingPostSnapshot;
@@ -1952,13 +3728,13 @@ namespace DxMessaging.Core.MessageBus
1952
3728
  )
1953
3729
  where TMessage : ITargetedMessage
1954
3730
  {
1955
- if (cache.handlers.Count == 0)
3731
+ // Snapshot semantics: see comment on RunBroadcast.
3732
+ List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
3733
+ int messageHandlersCount = messageHandlers.Count;
3734
+ if (messageHandlersCount == 0)
1956
3735
  {
1957
3736
  return;
1958
3737
  }
1959
-
1960
- List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
1961
- int messageHandlersCount = messageHandlers.Count;
1962
3738
  switch (messageHandlersCount)
1963
3739
  {
1964
3740
  case 1:
@@ -2108,13 +3884,13 @@ namespace DxMessaging.Core.MessageBus
2108
3884
  )
2109
3885
  where TMessage : ITargetedMessage
2110
3886
  {
2111
- if (cache.handlers.Count == 0)
3887
+ // Snapshot semantics: see comment on RunBroadcast.
3888
+ List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
3889
+ int messageHandlersCount = messageHandlers.Count;
3890
+ if (messageHandlersCount == 0)
2112
3891
  {
2113
3892
  return;
2114
3893
  }
2115
-
2116
- List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
2117
- int messageHandlersCount = messageHandlers.Count;
2118
3894
  switch (messageHandlersCount)
2119
3895
  {
2120
3896
  case 1:
@@ -2184,13 +3960,13 @@ namespace DxMessaging.Core.MessageBus
2184
3960
  )
2185
3961
  where TMessage : ITargetedMessage
2186
3962
  {
2187
- if (cache.handlers.Count == 0)
3963
+ // Snapshot semantics: see comment on RunBroadcast.
3964
+ List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
3965
+ int messageHandlersCount = messageHandlers.Count;
3966
+ if (messageHandlersCount == 0)
2188
3967
  {
2189
3968
  return;
2190
3969
  }
2191
-
2192
- List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
2193
- int messageHandlersCount = messageHandlers.Count;
2194
3970
  switch (messageHandlersCount)
2195
3971
  {
2196
3972
  case 1:
@@ -2274,10 +4050,13 @@ namespace DxMessaging.Core.MessageBus
2274
4050
  public void SourcedBroadcast<TMessage>(ref InstanceId source, ref TMessage typedMessage)
2275
4051
  where TMessage : IBroadcastMessage
2276
4052
  {
4053
+ TrySweepIdle();
4054
+ using DispatchLease dispatchLease = EnterDispatch();
2277
4055
  unchecked
2278
4056
  {
2279
4057
  _emissionId++;
2280
4058
  }
4059
+ long touchTick = AdvanceTick();
2281
4060
  if (_diagnosticsMode)
2282
4061
  {
2283
4062
  _emissionBuffer.Add(new MessageEmissionData(typedMessage, source));
@@ -2287,12 +4066,13 @@ namespace DxMessaging.Core.MessageBus
2287
4066
  DispatchSnapshot broadcastPostSnapshot = DispatchSnapshot.Empty;
2288
4067
  DispatchSnapshot broadcastWithoutSourcePostSnapshot = DispatchSnapshot.Empty;
2289
4068
  if (
2290
- _postProcessingBroadcastSinks.TryGetValue<TMessage>(
2291
- out Dictionary<
2292
- InstanceId,
2293
- HandlerCache<int, HandlerCache>
2294
- > broadcastPostHandlers
2295
- )
4069
+ _contextSinks[BusContextIndex.BroadcastPostProcessDefault]
4070
+ .TryGetValue<TMessage>(
4071
+ out Dictionary<
4072
+ InstanceId,
4073
+ HandlerCache<int, HandlerCache>
4074
+ > broadcastPostHandlers
4075
+ )
2296
4076
  && broadcastPostHandlers.TryGetValue(
2297
4077
  source,
2298
4078
  out HandlerCache<int, HandlerCache> broadcastPostByPriority
@@ -2300,25 +4080,28 @@ namespace DxMessaging.Core.MessageBus
2300
4080
  && broadcastPostByPriority.handlers.Count > 0
2301
4081
  )
2302
4082
  {
4083
+ Touch(broadcastPostByPriority, touchTick);
2303
4084
  broadcastPostSnapshot = AcquireDispatchSnapshot<TMessage>(
2304
4085
  this,
2305
4086
  broadcastPostByPriority,
2306
- DispatchCategory.BroadcastPost,
4087
+ BroadcastPostSlot,
2307
4088
  _emissionId
2308
4089
  );
2309
4090
  PrefreezeBroadcastPostSnapshot<TMessage>(ref source, broadcastPostSnapshot);
2310
4091
  }
2311
4092
  if (
2312
- _postProcessingBroadcastWithoutSourceSinks.TryGetValue<TMessage>(
2313
- out HandlerCache<int, HandlerCache> broadcastWithoutSourceHandlers
2314
- )
4093
+ _scalarSinks[BusSinkIndex.BroadcastPostProcessWithoutContext]
4094
+ .TryGetValue<TMessage>(
4095
+ out HandlerCache<int, HandlerCache> broadcastWithoutSourceHandlers
4096
+ )
2315
4097
  && broadcastWithoutSourceHandlers.handlers.Count > 0
2316
4098
  )
2317
4099
  {
4100
+ Touch(broadcastWithoutSourceHandlers, touchTick);
2318
4101
  broadcastWithoutSourcePostSnapshot = AcquireDispatchSnapshot<TMessage>(
2319
4102
  this,
2320
4103
  broadcastWithoutSourceHandlers,
2321
- DispatchCategory.BroadcastWithoutSourcePost,
4104
+ BroadcastWithoutContextPostSlot,
2322
4105
  _emissionId
2323
4106
  );
2324
4107
  PrefreezeBroadcastWithoutSourcePostSnapshot<TMessage>(
@@ -2331,46 +4114,65 @@ namespace DxMessaging.Core.MessageBus
2331
4114
  return;
2332
4115
  }
2333
4116
 
2334
- if (0 < _globalSinks.handlers.Count)
4117
+ if (0 < _globalSlots.sharedHandlers.Count)
2335
4118
  {
2336
4119
  IBroadcastMessage broadcastMessage = typedMessage;
2337
4120
  BroadcastGlobalSourcedBroadcast(ref source, ref broadcastMessage);
2338
4121
  }
2339
4122
 
2340
- // Pre-freeze broadcast-without-source handler stacks for this emission
4123
+ // Pre-freeze broadcast-without-source handler stacks for this emission.
4124
+ // Skip the prefreeze pass entirely when there is exactly one priority
4125
+ // bucket with at most one MessageHandler entry; see the rationale on
4126
+ // the snapshot-level Prefreeze*Snapshot fast-path short-circuit.
2341
4127
  if (
2342
- _sinks.TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> bwsHandlers)
4128
+ _scalarSinks[BusSinkIndex.BroadcastHandleWithoutContext]
4129
+ .TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> bwsHandlers)
2343
4130
  && bwsHandlers.handlers.Count > 0
2344
4131
  )
2345
4132
  {
4133
+ Touch(bwsHandlers, touchTick);
2346
4134
  List<KeyValuePair<int, HandlerCache>> frozen = GetOrAddMessageHandlerStack(
2347
4135
  bwsHandlers,
2348
4136
  _emissionId
2349
4137
  );
2350
4138
  int frozenCount = frozen.Count;
2351
- for (int i = 0; i < frozenCount; ++i)
4139
+ bool needsBwsPrefreeze = frozenCount > 1;
4140
+ List<MessageHandler> singleBucketBwsHandlers = null;
4141
+ if (!needsBwsPrefreeze && frozenCount == 1)
2352
4142
  {
2353
- KeyValuePair<int, HandlerCache> entry = frozen[i];
2354
- List<MessageHandler> mhList = GetOrAddMessageHandlerStack(
2355
- entry.Value,
4143
+ singleBucketBwsHandlers = GetOrAddMessageHandlerStack(
4144
+ frozen[0].Value,
2356
4145
  _emissionId
2357
4146
  );
2358
- for (int h = 0; h < mhList.Count; ++h)
4147
+ needsBwsPrefreeze = singleBucketBwsHandlers.Count > 1;
4148
+ }
4149
+ if (needsBwsPrefreeze)
4150
+ {
4151
+ for (int i = 0; i < frozenCount; ++i)
2359
4152
  {
2360
- mhList[h]
2361
- .PrefreezeBroadcastWithoutSourceHandlersForEmission<TMessage>(
2362
- entry.Key,
2363
- _emissionId,
2364
- this
2365
- );
4153
+ KeyValuePair<int, HandlerCache> entry = frozen[i];
4154
+ List<MessageHandler> mhList =
4155
+ (i == 0 && singleBucketBwsHandlers != null)
4156
+ ? singleBucketBwsHandlers
4157
+ : GetOrAddMessageHandlerStack(entry.Value, _emissionId);
4158
+ for (int h = 0; h < mhList.Count; ++h)
4159
+ {
4160
+ mhList[h]
4161
+ .PrefreezeBroadcastWithoutSourceHandlersForEmission<TMessage>(
4162
+ entry.Key,
4163
+ _emissionId,
4164
+ this
4165
+ );
4166
+ }
2366
4167
  }
2367
4168
  }
2368
4169
  }
2369
4170
 
2370
4171
  bool foundAnyHandlers = false;
2371
- _ = _broadcastSinks.TryGetValue<TMessage>(
2372
- out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> broadcastHandlers
2373
- );
4172
+ _ = _contextSinks[BusContextIndex.BroadcastHandleDefault]
4173
+ .TryGetValue<TMessage>(
4174
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> broadcastHandlers
4175
+ );
2374
4176
  if (
2375
4177
  broadcastHandlers != null
2376
4178
  && broadcastHandlers.TryGetValue(
@@ -2380,12 +4182,63 @@ namespace DxMessaging.Core.MessageBus
2380
4182
  && 0 < sortedHandlers.handlers.Count
2381
4183
  )
2382
4184
  {
4185
+ Touch(sortedHandlers, touchTick);
2383
4186
  foundAnyHandlers = true;
2384
4187
  List<KeyValuePair<int, HandlerCache>> handlerList = GetOrAddMessageHandlerStack(
2385
4188
  sortedHandlers,
2386
4189
  _emissionId
2387
4190
  );
2388
4191
  int handlerListCount = handlerList.Count;
4192
+ // Pre-freeze the typed-handler caches across every priority bucket so
4193
+ // deregistrations performed by an earlier priority's handler cannot
4194
+ // empty a later priority's stack mid-emission. The prefreeze pass is
4195
+ // only required when at least one later-running handler reads from a
4196
+ // cache that an earlier-running handler can mutate. That is the case
4197
+ // when there are multiple priority buckets, OR when the single bucket
4198
+ // holds more than one MessageHandler (each MessageHandler owns its
4199
+ // own typed-handler cache, so a removal in one can blank another).
4200
+ // Single-priority single-MessageHandler dispatch is already protected
4201
+ // by the lazy GetOrAddNewHandlerStack inside the dispatch path;
4202
+ // multiple delegate registrations within the same priority on the
4203
+ // same MessageHandler share a HandlerActionCache that is frozen on
4204
+ // first read by RunFastHandlersWithContext / RunHandlersWithContext.
4205
+ bool needsPrefreeze = handlerListCount > 1;
4206
+ List<MessageHandler> singleBucketFrozenHandlers = null;
4207
+ if (!needsPrefreeze && handlerListCount == 1)
4208
+ {
4209
+ // For the single-bucket case, count entries in the FROZEN
4210
+ // MessageHandler stack (not the live dict, which a concurrent
4211
+ // global/interceptor deregistration could shrink between snapshot
4212
+ // acquisition and this read). Reusing the frozen list also avoids
4213
+ // re-acquiring it inside the prefreeze loop below.
4214
+ singleBucketFrozenHandlers = GetOrAddMessageHandlerStack(
4215
+ handlerList[0].Value,
4216
+ _emissionId
4217
+ );
4218
+ needsPrefreeze = singleBucketFrozenHandlers.Count > 1;
4219
+ }
4220
+ if (needsPrefreeze)
4221
+ {
4222
+ for (int i = 0; i < handlerListCount; ++i)
4223
+ {
4224
+ KeyValuePair<int, HandlerCache> prefreezeEntry = handlerList[i];
4225
+ List<MessageHandler> prefreezeHandlers =
4226
+ (i == 0 && singleBucketFrozenHandlers != null)
4227
+ ? singleBucketFrozenHandlers
4228
+ : GetOrAddMessageHandlerStack(prefreezeEntry.Value, _emissionId);
4229
+ int prefreezeHandlerCount = prefreezeHandlers.Count;
4230
+ for (int h = 0; h < prefreezeHandlerCount; ++h)
4231
+ {
4232
+ prefreezeHandlers[h]
4233
+ .PrefreezeBroadcastHandlersForEmission<TMessage>(
4234
+ source,
4235
+ prefreezeEntry.Key,
4236
+ _emissionId,
4237
+ this
4238
+ );
4239
+ }
4240
+ }
4241
+ }
2389
4242
  switch (handlerListCount)
2390
4243
  {
2391
4244
  case 1:
@@ -2451,23 +4304,13 @@ namespace DxMessaging.Core.MessageBus
2451
4304
  }
2452
4305
  }
2453
4306
 
2454
- bool bwsFound = InternalBroadcastWithoutSource(ref source, ref typedMessage);
2455
-
2456
- if (
2457
- _postProcessingBroadcastSinks.TryGetValue<TMessage>(out broadcastHandlers)
2458
- && broadcastHandlers.TryGetValue(source, out sortedHandlers)
2459
- && 0 < sortedHandlers.handlers.Count
2460
- )
2461
- {
2462
- foundAnyHandlers = true;
2463
- DispatchSnapshot snapshot = AcquireDispatchSnapshot<TMessage>(
2464
- this,
2465
- sortedHandlers,
2466
- DispatchCategory.BroadcastPost,
2467
- _emissionId
2468
- );
2469
- DispatchBucket[] buckets = snapshot.buckets;
2470
- int bucketCount = snapshot.bucketCount;
4307
+ bool bwsFound = InternalBroadcastWithoutSource(ref source, ref typedMessage);
4308
+
4309
+ if (!broadcastPostSnapshot.IsEmpty)
4310
+ {
4311
+ foundAnyHandlers = true;
4312
+ DispatchBucket[] buckets = broadcastPostSnapshot.buckets;
4313
+ int bucketCount = broadcastPostSnapshot.bucketCount;
2471
4314
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
2472
4315
  {
2473
4316
  DispatchBucket bucket = buckets[bucketIndex];
@@ -2606,19 +4449,10 @@ namespace DxMessaging.Core.MessageBus
2606
4449
  }
2607
4450
  }
2608
4451
 
2609
- if (
2610
- _postProcessingBroadcastWithoutSourceSinks.TryGetValue<TMessage>(out sortedHandlers)
2611
- && 0 < sortedHandlers.handlers.Count
2612
- )
4452
+ if (!broadcastWithoutSourcePostSnapshot.IsEmpty)
2613
4453
  {
2614
- DispatchSnapshot snapshot = AcquireDispatchSnapshot<TMessage>(
2615
- this,
2616
- sortedHandlers,
2617
- DispatchCategory.BroadcastWithoutSourcePost,
2618
- _emissionId
2619
- );
2620
- DispatchBucket[] buckets = snapshot.buckets;
2621
- int bucketCount = snapshot.bucketCount;
4454
+ DispatchBucket[] buckets = broadcastWithoutSourcePostSnapshot.buckets;
4455
+ int bucketCount = broadcastWithoutSourcePostSnapshot.bucketCount;
2622
4456
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
2623
4457
  {
2624
4458
  DispatchBucket bucket = buckets[bucketIndex];
@@ -2776,12 +4610,13 @@ namespace DxMessaging.Core.MessageBus
2776
4610
  )
2777
4611
  where TMessage : IBroadcastMessage
2778
4612
  {
2779
- if (cache.handlers.Count == 0)
4613
+ // Snapshot semantics: see comment on RunBroadcast.
4614
+ List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
4615
+ int messageHandlersCount = messageHandlers.Count;
4616
+ if (messageHandlersCount == 0)
2780
4617
  {
2781
4618
  return;
2782
4619
  }
2783
- List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
2784
- int messageHandlersCount = messageHandlers.Count;
2785
4620
  switch (messageHandlersCount)
2786
4621
  {
2787
4622
  case 1:
@@ -2931,13 +4766,19 @@ namespace DxMessaging.Core.MessageBus
2931
4766
  )
2932
4767
  where TMessage : IBroadcastMessage
2933
4768
  {
2934
- if (cache.handlers.Count == 0)
4769
+ // Snapshot semantics: dispatch must respect the per-emission frozen
4770
+ // MessageHandler list, even if a handler running earlier in the same
4771
+ // emission has emptied the live cache.handlers dictionary by removing
4772
+ // its own (or a sibling priority's) registration. Reading the live
4773
+ // dict here would skip handlers that the snapshot still includes.
4774
+ // GetOrAddMessageHandlerStack returns the snapshot list; bail only
4775
+ // when that snapshot is empty.
4776
+ List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
4777
+ int messageHandlersCount = messageHandlers.Count;
4778
+ if (messageHandlersCount == 0)
2935
4779
  {
2936
4780
  return;
2937
4781
  }
2938
-
2939
- List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
2940
- int messageHandlersCount = messageHandlers.Count;
2941
4782
  switch (messageHandlersCount)
2942
4783
  {
2943
4784
  case 1:
@@ -3003,8 +4844,8 @@ namespace DxMessaging.Core.MessageBus
3003
4844
  {
3004
4845
  DispatchSnapshot snapshot = AcquireGlobalDispatchSnapshot<IUntargetedMessage>(
3005
4846
  this,
3006
- _globalSinks,
3007
- DispatchCategory.GlobalUntargeted,
4847
+ _globalSlots,
4848
+ DispatchKind.Untargeted,
3008
4849
  _emissionId
3009
4850
  );
3010
4851
  DispatchBucket[] buckets = snapshot.buckets;
@@ -3074,8 +4915,8 @@ namespace DxMessaging.Core.MessageBus
3074
4915
  {
3075
4916
  DispatchSnapshot snapshot = AcquireGlobalDispatchSnapshot<ITargetedMessage>(
3076
4917
  this,
3077
- _globalSinks,
3078
- DispatchCategory.GlobalTargeted,
4918
+ _globalSlots,
4919
+ DispatchKind.Targeted,
3079
4920
  _emissionId
3080
4921
  );
3081
4922
  DispatchBucket[] buckets = snapshot.buckets;
@@ -3148,8 +4989,8 @@ namespace DxMessaging.Core.MessageBus
3148
4989
  {
3149
4990
  DispatchSnapshot snapshot = AcquireGlobalDispatchSnapshot<IBroadcastMessage>(
3150
4991
  this,
3151
- _globalSinks,
3152
- DispatchCategory.GlobalBroadcast,
4992
+ _globalSlots,
4993
+ DispatchKind.Broadcast,
3153
4994
  _emissionId
3154
4995
  );
3155
4996
  DispatchBucket[] buckets = snapshot.buckets;
@@ -3426,10 +5267,11 @@ namespace DxMessaging.Core.MessageBus
3426
5267
  }
3427
5268
 
3428
5269
  private bool InternalUntargetedBroadcast<TMessage>(ref TMessage message)
3429
- where TMessage : IMessage
5270
+ where TMessage : IUntargetedMessage
3430
5271
  {
3431
5272
  if (
3432
- !_sinks.TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> sortedHandlers)
5273
+ !_scalarSinks[BusSinkIndex.UntargetedHandleDefault]
5274
+ .TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> sortedHandlers)
3433
5275
  || sortedHandlers.handlers.Count == 0
3434
5276
  )
3435
5277
  {
@@ -3439,7 +5281,7 @@ namespace DxMessaging.Core.MessageBus
3439
5281
  DispatchSnapshot snapshot = AcquireDispatchSnapshot<TMessage>(
3440
5282
  this,
3441
5283
  sortedHandlers,
3442
- DispatchCategory.Untargeted,
5284
+ UntargetedHandleSlot,
3443
5285
  _emissionId
3444
5286
  );
3445
5287
  DispatchBucket[] buckets = snapshot.buckets;
@@ -3450,6 +5292,11 @@ namespace DxMessaging.Core.MessageBus
3450
5292
  return false;
3451
5293
  }
3452
5294
 
5295
+ // Pre-freeze the typed-handler caches across every priority bucket so
5296
+ // deregistrations performed by an earlier priority's handler cannot
5297
+ // empty a later priority's stack mid-emission.
5298
+ PrefreezeUntargetedSnapshot<TMessage>(snapshot);
5299
+
3453
5300
  bool invoked = false;
3454
5301
 
3455
5302
  for (int i = 0; i < bucketCount; ++i)
@@ -3559,6 +5406,18 @@ namespace DxMessaging.Core.MessageBus
3559
5406
  return;
3560
5407
  }
3561
5408
 
5409
+ // No fast-path short-circuit for post-processor prefreeze.
5410
+ //
5411
+ // The single-bucket/single-entry fast-path used by handler prefreeze
5412
+ // (see PrefreezeUntargetedSnapshot) is unsafe for post-processors:
5413
+ // post-processors run AFTER regular handlers, and a regular handler
5414
+ // is allowed to register a NEW post-processor (or a new delegate on
5415
+ // an existing post-processor cache) during its own execution. Without
5416
+ // an unconditional prefreeze, the post-processor cache's first read
5417
+ // happens lazily inside the post-processor dispatch; by which time
5418
+ // the version has been bumped and the cache will be rebuilt with the
5419
+ // newly-registered entry visible. Always prefreezing pins the
5420
+ // emission-start snapshot before any handler can mutate it.
3562
5421
  DispatchBucket[] buckets = snapshot.buckets;
3563
5422
  int bucketCount = snapshot.bucketCount;
3564
5423
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
@@ -3584,6 +5443,55 @@ namespace DxMessaging.Core.MessageBus
3584
5443
  }
3585
5444
  }
3586
5445
 
5446
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
5447
+ private void PrefreezeUntargetedSnapshot<TMessage>(DispatchSnapshot snapshot)
5448
+ where TMessage : IUntargetedMessage
5449
+ {
5450
+ if (snapshot.IsEmpty)
5451
+ {
5452
+ return;
5453
+ }
5454
+
5455
+ // Prefreeze fast-path short-circuit: if there is exactly one priority
5456
+ // bucket with at most one MessageHandler entry, no later handler can
5457
+ // observe a removal performed by an earlier one, so the inline lazy
5458
+ // freeze inside the dispatch path is sufficient. Note: a single
5459
+ // MessageHandler may still register multiple delegates at the same
5460
+ // priority; those share a HandlerActionCache that is frozen on first
5461
+ // read by the per-priority RunFastHandlers/RunHandlers, so the lazy
5462
+ // freeze covers same-priority same-MessageHandler removals correctly.
5463
+ // See the longer rationale on the broadcast inline prefreeze block
5464
+ // in SourcedBroadcast.
5465
+ if (snapshot.bucketCount == 1 && snapshot.buckets[0].entryCount <= 1)
5466
+ {
5467
+ return;
5468
+ }
5469
+
5470
+ DispatchBucket[] buckets = snapshot.buckets;
5471
+ int bucketCount = snapshot.bucketCount;
5472
+ for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
5473
+ {
5474
+ DispatchBucket bucket = buckets[bucketIndex];
5475
+ DispatchEntry[] entries = bucket.entries;
5476
+ int entryCount = bucket.entryCount;
5477
+ if (entryCount == 0)
5478
+ {
5479
+ continue;
5480
+ }
5481
+
5482
+ int priority = bucket.priority;
5483
+ for (int entryIndex = 0; entryIndex < entryCount; ++entryIndex)
5484
+ {
5485
+ entries[entryIndex]
5486
+ .handler.PrefreezeUntargetedHandlersForEmission<TMessage>(
5487
+ priority,
5488
+ _emissionId,
5489
+ this
5490
+ );
5491
+ }
5492
+ }
5493
+ }
5494
+
3587
5495
  private bool InternalTargetedWithoutTargetingBroadcast<TMessage>(
3588
5496
  ref InstanceId target,
3589
5497
  ref TMessage message
@@ -3591,7 +5499,8 @@ namespace DxMessaging.Core.MessageBus
3591
5499
  where TMessage : ITargetedMessage
3592
5500
  {
3593
5501
  if (
3594
- !_sinks.TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> sortedHandlers)
5502
+ !_scalarSinks[BusSinkIndex.TargetedHandleWithoutContext]
5503
+ .TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> sortedHandlers)
3595
5504
  || sortedHandlers.handlers.Count == 0
3596
5505
  )
3597
5506
  {
@@ -3601,13 +5510,48 @@ namespace DxMessaging.Core.MessageBus
3601
5510
  DispatchSnapshot snapshot = AcquireDispatchSnapshot<TMessage>(
3602
5511
  this,
3603
5512
  sortedHandlers,
3604
- DispatchCategory.TargetedWithoutTargeting,
5513
+ TargetedWithoutContextHandleSlot,
3605
5514
  _emissionId
3606
5515
  );
3607
5516
  DispatchBucket[] buckets = snapshot.buckets;
3608
5517
  int bucketCount = snapshot.bucketCount;
3609
5518
  bool invoked = false;
3610
5519
 
5520
+ // Hoist per-MessageHandler prefreeze across ALL priority buckets
5521
+ // when there is more than one bucket. A handler running in an
5522
+ // earlier bucket can deregister a delegate that lives in a later
5523
+ // bucket's typed cache; if the later bucket's snapshot is taken
5524
+ // lazily inside its own dispatch (after the deregistration), the
5525
+ // rebuild will observe the mutation and the handler will be
5526
+ // skipped, violating snapshot semantics. The single-bucket case
5527
+ // is unchanged; no later bucket exists to be polluted, and the
5528
+ // inline per-bucket prefreeze below covers it.
5529
+ if (bucketCount > 1)
5530
+ {
5531
+ for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
5532
+ {
5533
+ DispatchBucket prefreezeBucket = buckets[bucketIndex];
5534
+ DispatchEntry[] prefreezeEntries = prefreezeBucket.entries;
5535
+ int prefreezeEntryCount = prefreezeBucket.entryCount;
5536
+ if (prefreezeEntryCount == 0)
5537
+ {
5538
+ continue;
5539
+ }
5540
+
5541
+ if (
5542
+ prefreezeEntries[0].prefreeze.kind
5543
+ == PrefreezeKindTargetedWithoutTargetingHandlers
5544
+ )
5545
+ {
5546
+ PrefreezeTargetedWithoutTargetingEntries<TMessage>(
5547
+ prefreezeEntries,
5548
+ prefreezeEntryCount,
5549
+ prefreezeBucket.priority
5550
+ );
5551
+ }
5552
+ }
5553
+ }
5554
+
3611
5555
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
3612
5556
  {
3613
5557
  DispatchBucket bucket = buckets[bucketIndex];
@@ -3620,7 +5564,14 @@ namespace DxMessaging.Core.MessageBus
3620
5564
 
3621
5565
  invoked = true;
3622
5566
  int priority = bucket.priority;
3623
- if (entries[0].prefreeze.kind == PrefreezeKindTargetedWithoutTargetingHandlers)
5567
+ // Inline per-bucket prefreeze for the single-bucket case only.
5568
+ // When bucketCount > 1 the hoisted pass above has already
5569
+ // prefrozen every bucket; running it again here would be
5570
+ // harmless but redundant.
5571
+ if (
5572
+ bucketCount == 1
5573
+ && entries[0].prefreeze.kind == PrefreezeKindTargetedWithoutTargetingHandlers
5574
+ )
3624
5575
  {
3625
5576
  PrefreezeTargetedWithoutTargetingEntries<TMessage>(
3626
5577
  entries,
@@ -3764,14 +5715,15 @@ namespace DxMessaging.Core.MessageBus
3764
5715
  )
3765
5716
  where TMessage : ITargetedMessage
3766
5717
  {
3767
- if (cache.handlers.Count == 0)
5718
+ // Snapshot semantics: see comment on RunBroadcast.
5719
+ List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
5720
+ int messageHandlersCount = messageHandlers.Count;
5721
+ if (messageHandlersCount == 0)
3768
5722
  {
3769
5723
  return;
3770
5724
  }
3771
-
3772
- List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
3773
5725
  // Freeze each handler's typed caches for this emission/priority to ensure snapshot semantics
3774
- for (int j = 0; j < messageHandlers.Count; ++j)
5726
+ for (int j = 0; j < messageHandlersCount; ++j)
3775
5727
  {
3776
5728
  messageHandlers[j]
3777
5729
  .PrefreezeTargetedWithoutTargetingHandlersForEmission<TMessage>(
@@ -3780,7 +5732,6 @@ namespace DxMessaging.Core.MessageBus
3780
5732
  this
3781
5733
  );
3782
5734
  }
3783
- int messageHandlersCount = messageHandlers.Count;
3784
5735
  switch (messageHandlersCount)
3785
5736
  {
3786
5737
  case 1:
@@ -3849,7 +5800,8 @@ namespace DxMessaging.Core.MessageBus
3849
5800
  where TMessage : IBroadcastMessage
3850
5801
  {
3851
5802
  if (
3852
- !_sinks.TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> sortedHandlers)
5803
+ !_scalarSinks[BusSinkIndex.BroadcastHandleWithoutContext]
5804
+ .TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> sortedHandlers)
3853
5805
  || sortedHandlers.handlers.Count == 0
3854
5806
  )
3855
5807
  {
@@ -3861,6 +5813,36 @@ namespace DxMessaging.Core.MessageBus
3861
5813
  _emissionId
3862
5814
  );
3863
5815
  int handlerListCount = handlerList.Count;
5816
+ // Hoist per-MessageHandler prefreeze across ALL priority buckets
5817
+ // when there is more than one bucket. A handler running in an
5818
+ // earlier bucket can deregister a delegate that lives in a later
5819
+ // bucket's typed cache; if the later bucket's snapshot is taken
5820
+ // lazily inside RunBroadcastWithoutSource (after the
5821
+ // deregistration), the rebuild will observe the mutation and
5822
+ // skip the handler, violating snapshot semantics. The
5823
+ // single-bucket case is unchanged; RunBroadcastWithoutSource's
5824
+ // inline prefreeze covers it.
5825
+ if (handlerListCount > 1)
5826
+ {
5827
+ for (int i = 0; i < handlerListCount; ++i)
5828
+ {
5829
+ KeyValuePair<int, HandlerCache> prefreezeEntry = handlerList[i];
5830
+ List<MessageHandler> mhList = GetOrAddMessageHandlerStack(
5831
+ prefreezeEntry.Value,
5832
+ _emissionId
5833
+ );
5834
+ int mhCount = mhList.Count;
5835
+ for (int h = 0; h < mhCount; ++h)
5836
+ {
5837
+ mhList[h]
5838
+ .PrefreezeBroadcastWithoutSourceHandlersForEmission<TMessage>(
5839
+ prefreezeEntry.Key,
5840
+ _emissionId,
5841
+ this
5842
+ );
5843
+ }
5844
+ }
5845
+ }
3864
5846
  switch (handlerListCount)
3865
5847
  {
3866
5848
  case 1:
@@ -3932,13 +5914,19 @@ namespace DxMessaging.Core.MessageBus
3932
5914
  )
3933
5915
  where TMessage : IBroadcastMessage
3934
5916
  {
3935
- if (cache.handlers.Count == 0)
5917
+ // Snapshot semantics: dispatch must respect the per-emission frozen
5918
+ // MessageHandler list, even if a handler running earlier in the same
5919
+ // emission has emptied the live cache.handlers dictionary by removing
5920
+ // its own (or a sibling priority's) registration. Reading the live
5921
+ // dict here would skip handlers that the snapshot still includes.
5922
+ // GetOrAddMessageHandlerStack returns the snapshot list; bail only
5923
+ // when that snapshot is empty.
5924
+ List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
5925
+ int messageHandlersCount = messageHandlers.Count;
5926
+ if (messageHandlersCount == 0)
3936
5927
  {
3937
5928
  return;
3938
5929
  }
3939
-
3940
- List<MessageHandler> messageHandlers = GetOrAddMessageHandlerStack(cache, _emissionId);
3941
- int messageHandlersCount = messageHandlers.Count;
3942
5930
  // Ensure each handler's typed no-source caches are frozen for this emission/priority
3943
5931
  for (int j = 0; j < messageHandlersCount; ++j)
3944
5932
  {
@@ -4103,10 +6091,12 @@ namespace DxMessaging.Core.MessageBus
4103
6091
  throw new ArgumentNullException(nameof(messageHandler));
4104
6092
  }
4105
6093
 
6094
+ long touchTick = AdvanceTick();
4106
6095
  InstanceId handlerOwnerId = messageHandler.owner;
4107
6096
  HandlerCache<int, HandlerCache> handlers = sinks.GetOrAdd<T>();
6097
+ Touch(handlers, touchTick);
4108
6098
  HandlerCache<int, HandlerCache> capturedHandlers = handlers;
4109
- DispatchCategory dispatchCategory = GetDispatchCategory(registrationMethod);
6099
+ SlotKey slotKey = RegistrationMethodAxes.GetSlotKey(registrationMethod);
4110
6100
 
4111
6101
  if (!handlers.handlers.TryGetValue(priority, out HandlerCache cache))
4112
6102
  {
@@ -4128,7 +6118,7 @@ namespace DxMessaging.Core.MessageBus
4128
6118
  int count = handler.GetValueOrDefault(messageHandler, 0);
4129
6119
 
4130
6120
  handler[messageHandler] = count + 1;
4131
- StageDispatchSnapshot<T>(this, capturedHandlers, dispatchCategory);
6121
+ StageDispatchSnapshot<T>(this, capturedHandlers, slotKey);
4132
6122
  Type type = typeof(T);
4133
6123
  _log.Log(
4134
6124
  new MessagingRegistration(
@@ -4139,23 +6129,35 @@ namespace DxMessaging.Core.MessageBus
4139
6129
  )
4140
6130
  );
4141
6131
 
6132
+ long capturedGeneration = _resetGeneration;
4142
6133
  return () =>
4143
6134
  {
6135
+ // Generation guard: if ResetState() ran after this closure was
6136
+ // captured (e.g. a deferred Object.Destroy fires after a
6137
+ // domain-reload-style reset), silently no-op rather than
6138
+ // logging a misleading over-deregistration error.
6139
+ if (capturedGeneration != _resetGeneration)
6140
+ {
6141
+ return;
6142
+ }
6143
+
6144
+ long deregisterTouchTick = AdvanceTick();
4144
6145
  cache.version++;
4145
- _log.Log(
4146
- new MessagingRegistration(
4147
- handlerOwnerId,
4148
- type,
4149
- RegistrationType.Deregister,
4150
- registrationMethod
4151
- )
4152
- );
4153
6146
  if (
4154
6147
  !sinks.TryGetValue<T>(out handlers)
6148
+ || !ReferenceEquals(handlers, capturedHandlers)
4155
6149
  || !handlers.handlers.TryGetValue(priority, out cache)
4156
6150
  || !cache.handlers.TryGetValue(messageHandler, out count)
4157
6151
  )
4158
6152
  {
6153
+ if (
6154
+ capturedHandlers.handlers.Count == 0
6155
+ && !ReferenceEquals(handlers, capturedHandlers)
6156
+ )
6157
+ {
6158
+ return;
6159
+ }
6160
+
4159
6161
  if (MessagingDebug.enabled)
4160
6162
  {
4161
6163
  MessagingDebug.Log(
@@ -4169,11 +6171,21 @@ namespace DxMessaging.Core.MessageBus
4169
6171
  return;
4170
6172
  }
4171
6173
 
6174
+ _log.Log(
6175
+ new MessagingRegistration(
6176
+ handlerOwnerId,
6177
+ type,
6178
+ RegistrationType.Deregister,
6179
+ registrationMethod
6180
+ )
6181
+ );
6182
+ Touch(handlers, deregisterTouchTick);
4172
6183
  handlers.version++;
4173
6184
  handler = cache.handlers;
4174
6185
  if (count <= 1)
4175
6186
  {
4176
6187
  bool complete = handler.Remove(messageHandler);
6188
+ MarkDirtyHandler(messageHandler);
4177
6189
  cache.version++;
4178
6190
  // do not mutate cache.cache here; let next read rebuild from handlers
4179
6191
 
@@ -4191,7 +6203,7 @@ namespace DxMessaging.Core.MessageBus
4191
6203
 
4192
6204
  if (handlers.handlers.Count == 0)
4193
6205
  {
4194
- sinks.Remove<T>();
6206
+ MarkDirtyType<T>();
4195
6207
  }
4196
6208
 
4197
6209
  if (!complete && MessagingDebug.enabled)
@@ -4208,7 +6220,7 @@ namespace DxMessaging.Core.MessageBus
4208
6220
  {
4209
6221
  handler[messageHandler] = count - 1;
4210
6222
  }
4211
- StageDispatchSnapshot<T>(this, handlers, dispatchCategory);
6223
+ StageDispatchSnapshot<T>(this, handlers, slotKey);
4212
6224
  };
4213
6225
  }
4214
6226
 
@@ -4226,9 +6238,12 @@ namespace DxMessaging.Core.MessageBus
4226
6238
  throw new ArgumentNullException(nameof(messageHandler));
4227
6239
  }
4228
6240
 
6241
+ long touchTick = AdvanceTick();
4229
6242
  Dictionary<InstanceId, HandlerCache<int, HandlerCache>> broadcastHandlers =
4230
- sinks.GetOrAdd<T>();
4231
- DispatchCategory dispatchCategory = GetDispatchCategory(registrationMethod);
6243
+ GetOrRentContextMap<T>(sinks);
6244
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> capturedBroadcastHandlers =
6245
+ broadcastHandlers;
6246
+ SlotKey slotKey = RegistrationMethodAxes.GetSlotKey(registrationMethod);
4232
6247
 
4233
6248
  if (
4234
6249
  !broadcastHandlers.TryGetValue(
@@ -4239,7 +6254,10 @@ namespace DxMessaging.Core.MessageBus
4239
6254
  {
4240
6255
  handlers = new HandlerCache<int, HandlerCache>();
4241
6256
  broadcastHandlers[context] = handlers;
6257
+ TrackContextMapHighWater(broadcastHandlers);
4242
6258
  }
6259
+ Touch(handlers, touchTick);
6260
+ HandlerCache<int, HandlerCache> capturedHandlers = handlers;
4243
6261
 
4244
6262
  if (!handlers.handlers.TryGetValue(priority, out HandlerCache cache))
4245
6263
  {
@@ -4271,26 +6289,34 @@ namespace DxMessaging.Core.MessageBus
4271
6289
  registrationMethod
4272
6290
  )
4273
6291
  );
4274
- StageDispatchSnapshot<T>(this, handlers, dispatchCategory);
6292
+ StageDispatchSnapshot<T>(this, handlers, slotKey);
4275
6293
 
6294
+ long capturedGeneration = _resetGeneration;
4276
6295
  return () =>
4277
6296
  {
6297
+ // Generation guard: see InternalRegisterUntargeted for the
6298
+ // rationale. Skip silently when the closure outlived a Reset.
6299
+ if (capturedGeneration != _resetGeneration)
6300
+ {
6301
+ return;
6302
+ }
6303
+
6304
+ long deregisterTouchTick = AdvanceTick();
4278
6305
  cache.version++;
4279
- _log.Log(
4280
- new MessagingRegistration(
4281
- context,
4282
- type,
4283
- RegistrationType.Deregister,
4284
- registrationMethod
4285
- )
4286
- );
4287
6306
  if (
4288
6307
  !sinks.TryGetValue<T>(out broadcastHandlers)
6308
+ || !ReferenceEquals(broadcastHandlers, capturedBroadcastHandlers)
4289
6309
  || !broadcastHandlers.TryGetValue(context, out handlers)
6310
+ || !ReferenceEquals(handlers, capturedHandlers)
4290
6311
  || !handlers.handlers.TryGetValue(priority, out cache)
4291
6312
  || !cache.handlers.TryGetValue(messageHandler, out count)
4292
6313
  )
4293
6314
  {
6315
+ if (IsStaleContextDeregisterAfterSweep<T>(sinks, context, capturedHandlers))
6316
+ {
6317
+ return;
6318
+ }
6319
+
4294
6320
  if (MessagingDebug.enabled)
4295
6321
  {
4296
6322
  MessagingDebug.Log(
@@ -4304,10 +6330,20 @@ namespace DxMessaging.Core.MessageBus
4304
6330
  return;
4305
6331
  }
4306
6332
 
6333
+ _log.Log(
6334
+ new MessagingRegistration(
6335
+ context,
6336
+ type,
6337
+ RegistrationType.Deregister,
6338
+ registrationMethod
6339
+ )
6340
+ );
6341
+ Touch(handlers, deregisterTouchTick);
4307
6342
  handler = cache.handlers;
4308
6343
  if (count <= 1)
4309
6344
  {
4310
6345
  bool complete = handler.Remove(messageHandler);
6346
+ MarkDirtyHandler(messageHandler);
4311
6347
  cache.version++;
4312
6348
  // do not mutate cache.cache here; let next read rebuild from handlers
4313
6349
  if (handler.Count == 0)
@@ -4325,12 +6361,7 @@ namespace DxMessaging.Core.MessageBus
4325
6361
 
4326
6362
  if (handlers.handlers.Count == 0)
4327
6363
  {
4328
- _ = broadcastHandlers.Remove(context);
4329
- }
4330
-
4331
- if (broadcastHandlers.Count == 0)
4332
- {
4333
- sinks.Remove<T>();
6364
+ MarkDirtyTarget<T>(context);
4334
6365
  }
4335
6366
 
4336
6367
  if (!complete && MessagingDebug.enabled)
@@ -4347,26 +6378,44 @@ namespace DxMessaging.Core.MessageBus
4347
6378
  {
4348
6379
  handler[messageHandler] = count - 1;
4349
6380
  }
4350
- StageDispatchSnapshot<T>(this, handlers, dispatchCategory);
6381
+ StageDispatchSnapshot<T>(this, handlers, slotKey);
4351
6382
  };
4352
6383
  }
4353
6384
 
6385
+ private static bool IsStaleContextDeregisterAfterSweep<T>(
6386
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks,
6387
+ InstanceId context,
6388
+ HandlerCache<int, HandlerCache> capturedHandlers
6389
+ )
6390
+ where T : IMessage
6391
+ {
6392
+ return capturedHandlers.handlers.Count == 0
6393
+ && (
6394
+ !sinks.TryGetValue<T>(
6395
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> currentByContext
6396
+ )
6397
+ || !currentByContext.TryGetValue(
6398
+ context,
6399
+ out HandlerCache<int, HandlerCache> currentHandlers
6400
+ )
6401
+ || !ReferenceEquals(currentHandlers, capturedHandlers)
6402
+ );
6403
+ }
6404
+
4354
6405
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
4355
6406
  private static void StageDispatchSnapshot<TMessage>(
4356
6407
  MessageBus messageBus,
4357
6408
  HandlerCache<int, HandlerCache> handlers,
4358
- DispatchCategory category
6409
+ SlotKey slotKey
4359
6410
  )
4360
6411
  where TMessage : IMessage
4361
6412
  {
4362
- if (handlers == null || category == DispatchCategory.None)
6413
+ if (handlers == null || slotKey == SlotKey.None)
4363
6414
  {
4364
6415
  return;
4365
6416
  }
4366
6417
 
4367
- HandlerCache<int, HandlerCache>.DispatchState state = handlers.GetOrCreateDispatchState(
4368
- category
4369
- );
6418
+ DispatchState state = handlers.dispatchState ??= new DispatchState();
4370
6419
  if (state.hasPending)
4371
6420
  {
4372
6421
  ReleaseSnapshot(ref state.pending);
@@ -4378,17 +6427,23 @@ namespace DxMessaging.Core.MessageBus
4378
6427
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
4379
6428
  private static void StageGlobalDispatchSnapshot<TMessage>(
4380
6429
  MessageBus messageBus,
4381
- HandlerCache handlers,
4382
- DispatchCategory category
6430
+ BusGlobalSlot handlers,
6431
+ DispatchKind kind
4383
6432
  )
4384
6433
  where TMessage : IMessage
4385
6434
  {
4386
- if (handlers == null || category == DispatchCategory.None)
6435
+ // DispatchKind has no None sentinel; the bus only reaches this path
6436
+ // through register sites that pass a valid kind, so the legacy
6437
+ // category-None short-circuit is no longer needed -- the
6438
+ // `handlers == null` guard alone suffices.
6439
+ if (handlers == null)
4387
6440
  {
4388
6441
  return;
4389
6442
  }
4390
6443
 
4391
- HandlerCache.DispatchState state = handlers.GetOrCreateDispatchState(category);
6444
+ ref DispatchState slotState = ref SelectGlobalDispatchState(handlers, kind);
6445
+ slotState ??= new DispatchState();
6446
+ DispatchState state = slotState;
4392
6447
  if (state.hasPending)
4393
6448
  {
4394
6449
  ReleaseSnapshot(ref state.pending);
@@ -4398,6 +6453,29 @@ namespace DxMessaging.Core.MessageBus
4398
6453
  state.pendingDirty = true;
4399
6454
  }
4400
6455
 
6456
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
6457
+ private static ref DispatchState SelectGlobalDispatchState(
6458
+ BusGlobalSlot slot,
6459
+ DispatchKind kind
6460
+ )
6461
+ {
6462
+ switch (kind)
6463
+ {
6464
+ case DispatchKind.Untargeted:
6465
+ return ref slot.untargetedDispatchState;
6466
+ case DispatchKind.Targeted:
6467
+ return ref slot.targetedDispatchState;
6468
+ case DispatchKind.Broadcast:
6469
+ return ref slot.broadcastDispatchState;
6470
+ default:
6471
+ throw new ArgumentOutOfRangeException(
6472
+ nameof(kind),
6473
+ kind,
6474
+ "SelectGlobalDispatchState only supports Untargeted, Targeted, Broadcast."
6475
+ );
6476
+ }
6477
+ }
6478
+
4401
6479
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
4402
6480
  private static void ReleaseSnapshot(ref DispatchSnapshot snapshot)
4403
6481
  {
@@ -4414,7 +6492,7 @@ namespace DxMessaging.Core.MessageBus
4414
6492
  private static DispatchSnapshot AcquireDispatchSnapshot<TMessage>(
4415
6493
  MessageBus messageBus,
4416
6494
  HandlerCache<int, HandlerCache> handlers,
4417
- DispatchCategory category,
6495
+ SlotKey slotKey,
4418
6496
  long emissionId
4419
6497
  )
4420
6498
  where TMessage : IMessage
@@ -4424,14 +6502,13 @@ namespace DxMessaging.Core.MessageBus
4424
6502
  return DispatchSnapshot.Empty;
4425
6503
  }
4426
6504
 
4427
- if (category == DispatchCategory.None)
6505
+ if (slotKey == SlotKey.None)
4428
6506
  {
4429
6507
  return DispatchSnapshot.Empty;
4430
6508
  }
4431
6509
 
4432
- HandlerCache<int, HandlerCache>.DispatchState state = handlers.GetOrCreateDispatchState(
4433
- category
4434
- );
6510
+ Touch(handlers, messageBus._tickCounter);
6511
+ DispatchState state = handlers.dispatchState ??= new DispatchState();
4435
6512
 
4436
6513
  bool hasHandlers = handlers.handlers.Count > 0;
4437
6514
 
@@ -4441,7 +6518,7 @@ namespace DxMessaging.Core.MessageBus
4441
6518
  {
4442
6519
  ReleaseSnapshot(ref state.pending);
4443
6520
  state.pending = hasHandlers
4444
- ? BuildDispatchSnapshot<TMessage>(messageBus, handlers, category)
6521
+ ? BuildDispatchSnapshot<TMessage>(messageBus, handlers, slotKey)
4445
6522
  : DispatchSnapshot.Empty;
4446
6523
 
4447
6524
  state.pendingDirty = false;
@@ -4450,7 +6527,7 @@ namespace DxMessaging.Core.MessageBus
4450
6527
  else if (state.active.IsEmpty && hasHandlers)
4451
6528
  {
4452
6529
  ReleaseSnapshot(ref state.pending);
4453
- state.pending = BuildDispatchSnapshot<TMessage>(messageBus, handlers, category);
6530
+ state.pending = BuildDispatchSnapshot<TMessage>(messageBus, handlers, slotKey);
4454
6531
  state.hasPending = true;
4455
6532
  state.pendingDirty = false;
4456
6533
  }
@@ -4464,7 +6541,7 @@ namespace DxMessaging.Core.MessageBus
4464
6541
  {
4465
6542
  ReleaseSnapshot(ref state.pending);
4466
6543
  state.pending = hasHandlers
4467
- ? BuildDispatchSnapshot<TMessage>(messageBus, handlers, category)
6544
+ ? BuildDispatchSnapshot<TMessage>(messageBus, handlers, slotKey)
4468
6545
  : DispatchSnapshot.Empty;
4469
6546
 
4470
6547
  state.pendingDirty = false;
@@ -4490,7 +6567,7 @@ namespace DxMessaging.Core.MessageBus
4490
6567
  private static DispatchSnapshot BuildDispatchSnapshot<TMessage>(
4491
6568
  MessageBus messageBus,
4492
6569
  HandlerCache<int, HandlerCache> handlers,
4493
- DispatchCategory category
6570
+ SlotKey slotKey
4494
6571
  )
4495
6572
  where TMessage : IMessage
4496
6573
  {
@@ -4527,7 +6604,7 @@ namespace DxMessaging.Core.MessageBus
4527
6604
  FillDispatchEntries<TMessage>(
4528
6605
  messageBus,
4529
6606
  handlerLookup,
4530
- category,
6607
+ slotKey,
4531
6608
  priority,
4532
6609
  entries
4533
6610
  );
@@ -4540,7 +6617,7 @@ namespace DxMessaging.Core.MessageBus
4540
6617
  private static void FillDispatchEntries<TMessage>(
4541
6618
  MessageBus messageBus,
4542
6619
  Dictionary<MessageHandler, int> handlerLookup,
4543
- DispatchCategory category,
6620
+ SlotKey slotKey,
4544
6621
  int priority,
4545
6622
  DispatchEntry[] entries
4546
6623
  )
@@ -4551,12 +6628,12 @@ namespace DxMessaging.Core.MessageBus
4551
6628
  return;
4552
6629
  }
4553
6630
 
4554
- PrefreezeDescriptor prefreeze = CreatePrefreezeDescriptor(category, priority);
6631
+ PrefreezeDescriptor prefreeze = CreatePrefreezeDescriptor(slotKey, priority);
4555
6632
  int index = 0;
4556
6633
  foreach (KeyValuePair<MessageHandler, int> kvp in handlerLookup)
4557
6634
  {
4558
6635
  MessageHandler messageHandler = kvp.Key;
4559
- object dispatch = GetDispatchLink<TMessage>(messageBus, messageHandler, category);
6636
+ object dispatch = GetDispatchLink<TMessage>(messageBus, messageHandler, slotKey);
4560
6637
  entries[index++] = new DispatchEntry(messageHandler, dispatch, prefreeze);
4561
6638
  }
4562
6639
  if (index < entries.Length)
@@ -4568,19 +6645,22 @@ namespace DxMessaging.Core.MessageBus
4568
6645
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
4569
6646
  private static DispatchSnapshot AcquireGlobalDispatchSnapshot<TMessage>(
4570
6647
  MessageBus messageBus,
4571
- HandlerCache handlers,
4572
- DispatchCategory category,
6648
+ BusGlobalSlot handlers,
6649
+ DispatchKind kind,
4573
6650
  long emissionId
4574
6651
  )
4575
6652
  where TMessage : IMessage
4576
6653
  {
4577
- if (handlers == null || category == DispatchCategory.None)
6654
+ if (handlers == null)
4578
6655
  {
4579
6656
  return DispatchSnapshot.Empty;
4580
6657
  }
4581
6658
 
4582
- HandlerCache.DispatchState state = handlers.GetOrCreateDispatchState(category);
4583
- bool hasHandlers = handlers.handlers.Count > 0;
6659
+ handlers.lastTouchTicks = messageBus._tickCounter;
6660
+ ref DispatchState slotState = ref SelectGlobalDispatchState(handlers, kind);
6661
+ slotState ??= new DispatchState();
6662
+ DispatchState state = slotState;
6663
+ bool hasHandlers = handlers.sharedHandlers.Count > 0;
4584
6664
 
4585
6665
  if (state.hasPending)
4586
6666
  {
@@ -4592,7 +6672,7 @@ namespace DxMessaging.Core.MessageBus
4592
6672
  state.pending = BuildGlobalDispatchSnapshot<TMessage>(
4593
6673
  messageBus,
4594
6674
  handlers,
4595
- category
6675
+ kind
4596
6676
  );
4597
6677
  }
4598
6678
  else
@@ -4606,11 +6686,7 @@ namespace DxMessaging.Core.MessageBus
4606
6686
  else if (state.active.IsEmpty && hasHandlers)
4607
6687
  {
4608
6688
  ReleaseSnapshot(ref state.pending);
4609
- state.pending = BuildGlobalDispatchSnapshot<TMessage>(
4610
- messageBus,
4611
- handlers,
4612
- category
4613
- );
6689
+ state.pending = BuildGlobalDispatchSnapshot<TMessage>(messageBus, handlers, kind);
4614
6690
  state.hasPending = true;
4615
6691
  state.pendingDirty = false;
4616
6692
  }
@@ -4624,7 +6700,7 @@ namespace DxMessaging.Core.MessageBus
4624
6700
  {
4625
6701
  ReleaseSnapshot(ref state.pending);
4626
6702
  state.pending = hasHandlers
4627
- ? BuildGlobalDispatchSnapshot<TMessage>(messageBus, handlers, category)
6703
+ ? BuildGlobalDispatchSnapshot<TMessage>(messageBus, handlers, kind)
4628
6704
  : DispatchSnapshot.Empty;
4629
6705
 
4630
6706
  state.pendingDirty = false;
@@ -4649,26 +6725,31 @@ namespace DxMessaging.Core.MessageBus
4649
6725
 
4650
6726
  private static DispatchSnapshot BuildGlobalDispatchSnapshot<TMessage>(
4651
6727
  MessageBus messageBus,
4652
- HandlerCache handlers,
4653
- DispatchCategory category
6728
+ BusGlobalSlot handlers,
6729
+ DispatchKind kind
4654
6730
  )
4655
6731
  where TMessage : IMessage
4656
6732
  {
4657
- if (handlers == null || handlers.handlers.Count == 0)
6733
+ if (handlers == null || handlers.sharedHandlers.Count == 0)
4658
6734
  {
4659
6735
  return DispatchSnapshot.Empty;
4660
6736
  }
4661
6737
 
4662
6738
  DispatchBucket[] buckets = DispatchBucketPool.Rent(1);
4663
- Dictionary<MessageHandler, int> handlerLookup = handlers.handlers;
6739
+ Dictionary<MessageHandler, int> handlerLookup = handlers.sharedHandlers;
4664
6740
  int entryCount = handlerLookup.Count;
4665
6741
  DispatchEntry[] entries = DispatchEntryPool.Rent(entryCount);
4666
- PrefreezeDescriptor prefreeze = CreatePrefreezeDescriptor(category, 0);
6742
+ PrefreezeDescriptor prefreeze = CreateGlobalPrefreezeDescriptor(kind, 0);
4667
6743
  int index = 0;
4668
6744
  foreach (KeyValuePair<MessageHandler, int> kvp in handlerLookup)
4669
6745
  {
4670
6746
  MessageHandler messageHandler = kvp.Key;
4671
- object dispatch = GetDispatchLink<TMessage>(messageBus, messageHandler, category);
6747
+ // Global dispatch paths intentionally pass null for the
6748
+ // dispatch-link argument. GetDispatchLink is no longer reached
6749
+ // from this code path; inlining null here matches what the
6750
+ // legacy switch returned for all three Global cases and avoids
6751
+ // a per-entry call.
6752
+ object dispatch = null;
4672
6753
  entries[index++] = new DispatchEntry(messageHandler, dispatch, prefreeze);
4673
6754
  }
4674
6755
 
@@ -4677,108 +6758,100 @@ namespace DxMessaging.Core.MessageBus
4677
6758
  }
4678
6759
 
4679
6760
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
4680
- private static PrefreezeDescriptor CreatePrefreezeDescriptor(
4681
- DispatchCategory category,
4682
- int priority
4683
- )
6761
+ private static PrefreezeDescriptor CreatePrefreezeDescriptor(SlotKey slotKey, int priority)
4684
6762
  {
4685
- switch (category)
6763
+ if (
6764
+ slotKey.Phase != DispatchPhase.Handle
6765
+ || slotKey.Variant != DispatchVariant.WithoutContext
6766
+ )
6767
+ {
6768
+ return PrefreezeDescriptor.Empty;
6769
+ }
6770
+ switch (slotKey.Kind)
4686
6771
  {
4687
- case DispatchCategory.TargetedWithoutTargeting:
6772
+ case DispatchKind.Targeted:
4688
6773
  return new PrefreezeDescriptor(
4689
6774
  PrefreezeKindTargetedWithoutTargetingHandlers,
4690
6775
  priority
4691
6776
  );
4692
- case DispatchCategory.BroadcastWithoutSource:
6777
+ case DispatchKind.Broadcast:
4693
6778
  return new PrefreezeDescriptor(
4694
6779
  PrefreezeKindBroadcastWithoutSourceHandlers,
4695
6780
  priority
4696
6781
  );
4697
- case DispatchCategory.GlobalUntargeted:
6782
+ default:
6783
+ return PrefreezeDescriptor.Empty;
6784
+ }
6785
+ }
6786
+
6787
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
6788
+ private static PrefreezeDescriptor CreateGlobalPrefreezeDescriptor(
6789
+ DispatchKind kind,
6790
+ int priority
6791
+ )
6792
+ {
6793
+ switch (kind)
6794
+ {
6795
+ case DispatchKind.Untargeted:
4698
6796
  return new PrefreezeDescriptor(PrefreezeKindGlobalUntargetedHandlers, priority);
4699
- case DispatchCategory.GlobalTargeted:
6797
+ case DispatchKind.Targeted:
4700
6798
  return new PrefreezeDescriptor(PrefreezeKindGlobalTargetedHandlers, priority);
4701
- case DispatchCategory.GlobalBroadcast:
6799
+ case DispatchKind.Broadcast:
4702
6800
  return new PrefreezeDescriptor(PrefreezeKindGlobalBroadcastHandlers, priority);
4703
6801
  default:
4704
- return PrefreezeDescriptor.Empty;
6802
+ throw new ArgumentOutOfRangeException(
6803
+ nameof(kind),
6804
+ kind,
6805
+ "CreateGlobalPrefreezeDescriptor only supports Untargeted, Targeted, Broadcast."
6806
+ );
4705
6807
  }
4706
6808
  }
4707
6809
 
4708
6810
  private static object GetDispatchLink<TMessage>(
4709
6811
  MessageBus messageBus,
4710
6812
  MessageHandler handler,
4711
- DispatchCategory category
6813
+ SlotKey slotKey
4712
6814
  )
4713
6815
  where TMessage : IMessage
4714
6816
  {
4715
- switch (category)
4716
- {
4717
- case DispatchCategory.Untargeted:
4718
- return handler.GetOrCreateUntargetedDispatchLink<TMessage>(messageBus);
4719
- case DispatchCategory.UntargetedPost:
4720
- return handler.GetOrCreateUntargetedPostDispatchLink<TMessage>(messageBus);
4721
- case DispatchCategory.Targeted:
4722
- return handler.GetOrCreateTargetedDispatchLink<TMessage>(messageBus);
4723
- case DispatchCategory.TargetedPost:
4724
- return handler.GetOrCreateTargetedPostDispatchLink<TMessage>(messageBus);
4725
- case DispatchCategory.TargetedWithoutTargeting:
4726
- return handler.GetOrCreateTargetedWithoutTargetingDispatchLink<TMessage>(
4727
- messageBus
4728
- );
4729
- case DispatchCategory.TargetedWithoutTargetingPost:
4730
- return handler.GetOrCreateTargetedWithoutTargetingPostDispatchLink<TMessage>(
4731
- messageBus
4732
- );
4733
- case DispatchCategory.Broadcast:
4734
- return handler.GetOrCreateBroadcastDispatchLink<TMessage>(messageBus);
4735
- case DispatchCategory.BroadcastPost:
4736
- return handler.GetOrCreateBroadcastPostDispatchLink<TMessage>(messageBus);
4737
- case DispatchCategory.BroadcastWithoutSource:
4738
- return handler.GetOrCreateBroadcastWithoutSourceDispatchLink<TMessage>(
4739
- messageBus
4740
- );
4741
- case DispatchCategory.BroadcastWithoutSourcePost:
4742
- return handler.GetOrCreateBroadcastWithoutSourcePostDispatchLink<TMessage>(
4743
- messageBus
4744
- );
4745
- case DispatchCategory.GlobalUntargeted:
4746
- case DispatchCategory.GlobalTargeted:
4747
- case DispatchCategory.GlobalBroadcast:
4748
- return null;
4749
- default:
4750
- return handler.GetOrCreateUntargetedDispatchLink<TMessage>(messageBus);
6817
+ DispatchKind kind = slotKey.Kind;
6818
+ DispatchPhase phase = slotKey.Phase;
6819
+ DispatchVariant variant = slotKey.Variant;
6820
+ if (kind == DispatchKind.Untargeted)
6821
+ {
6822
+ return phase == DispatchPhase.PostProcess
6823
+ ? handler.GetOrCreateUntargetedPostDispatchLink<TMessage>(messageBus)
6824
+ : handler.GetOrCreateUntargetedDispatchLink<TMessage>(messageBus);
4751
6825
  }
4752
- }
4753
-
4754
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
4755
- private static DispatchCategory GetDispatchCategory(RegistrationMethod registrationMethod)
4756
- {
4757
- switch (registrationMethod)
4758
- {
4759
- case RegistrationMethod.Untargeted:
4760
- return DispatchCategory.Untargeted;
4761
- case RegistrationMethod.UntargetedPostProcessor:
4762
- return DispatchCategory.UntargetedPost;
4763
- case RegistrationMethod.Targeted:
4764
- return DispatchCategory.Targeted;
4765
- case RegistrationMethod.TargetedPostProcessor:
4766
- return DispatchCategory.TargetedPost;
4767
- case RegistrationMethod.TargetedWithoutTargeting:
4768
- return DispatchCategory.TargetedWithoutTargeting;
4769
- case RegistrationMethod.TargetedWithoutTargetingPostProcessor:
4770
- return DispatchCategory.TargetedWithoutTargetingPost;
4771
- case RegistrationMethod.Broadcast:
4772
- return DispatchCategory.Broadcast;
4773
- case RegistrationMethod.BroadcastPostProcessor:
4774
- return DispatchCategory.BroadcastPost;
4775
- case RegistrationMethod.BroadcastWithoutSource:
4776
- return DispatchCategory.BroadcastWithoutSource;
4777
- case RegistrationMethod.BroadcastWithoutSourcePostProcessor:
4778
- return DispatchCategory.BroadcastWithoutSourcePost;
4779
- default:
4780
- return DispatchCategory.None;
6826
+ if (kind == DispatchKind.Targeted)
6827
+ {
6828
+ if (phase == DispatchPhase.PostProcess)
6829
+ {
6830
+ return variant == DispatchVariant.WithoutContext
6831
+ ? handler.GetOrCreateTargetedWithoutTargetingPostDispatchLink<TMessage>(
6832
+ messageBus
6833
+ )
6834
+ : handler.GetOrCreateTargetedPostDispatchLink<TMessage>(messageBus);
6835
+ }
6836
+ return variant == DispatchVariant.WithoutContext
6837
+ ? handler.GetOrCreateTargetedWithoutTargetingDispatchLink<TMessage>(messageBus)
6838
+ : handler.GetOrCreateTargetedDispatchLink<TMessage>(messageBus);
6839
+ }
6840
+ if (kind == DispatchKind.Broadcast)
6841
+ {
6842
+ if (phase == DispatchPhase.PostProcess)
6843
+ {
6844
+ return variant == DispatchVariant.WithoutContext
6845
+ ? handler.GetOrCreateBroadcastWithoutSourcePostDispatchLink<TMessage>(
6846
+ messageBus
6847
+ )
6848
+ : handler.GetOrCreateBroadcastPostDispatchLink<TMessage>(messageBus);
6849
+ }
6850
+ return variant == DispatchVariant.WithoutContext
6851
+ ? handler.GetOrCreateBroadcastWithoutSourceDispatchLink<TMessage>(messageBus)
6852
+ : handler.GetOrCreateBroadcastDispatchLink<TMessage>(messageBus);
4781
6853
  }
6854
+ return handler.GetOrCreateUntargetedDispatchLink<TMessage>(messageBus);
4782
6855
  }
4783
6856
 
4784
6857
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -4833,6 +6906,12 @@ namespace DxMessaging.Core.MessageBus
4833
6906
  return;
4834
6907
  }
4835
6908
 
6909
+ // No fast-path short-circuit for post-processor prefreeze. See the
6910
+ // detailed rationale on PrefreezeUntargetedPostSnapshot; a regular
6911
+ // handler can register a new post-processor (same MessageHandler,
6912
+ // same priority) during its own execution, and the lazy first-read
6913
+ // inside post-processor dispatch would otherwise capture that newly
6914
+ // added entry. Always prefreezing pins the emission-start snapshot.
4836
6915
  DispatchBucket[] buckets = snapshot.buckets;
4837
6916
  int bucketCount = snapshot.bucketCount;
4838
6917
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
@@ -4859,6 +6938,59 @@ namespace DxMessaging.Core.MessageBus
4859
6938
  }
4860
6939
  }
4861
6940
 
6941
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
6942
+ private void PrefreezeTargetedSnapshot<TMessage>(
6943
+ ref InstanceId target,
6944
+ DispatchSnapshot snapshot
6945
+ )
6946
+ where TMessage : ITargetedMessage
6947
+ {
6948
+ if (snapshot.IsEmpty)
6949
+ {
6950
+ return;
6951
+ }
6952
+
6953
+ // Prefreeze fast-path short-circuit: if there is exactly one priority
6954
+ // bucket with at most one MessageHandler entry, no later handler can
6955
+ // observe a removal performed by an earlier one, so the inline lazy
6956
+ // freeze inside the dispatch path is sufficient. Note: a single
6957
+ // MessageHandler may still register multiple delegates at the same
6958
+ // priority; those share a HandlerActionCache that is frozen on first
6959
+ // read by the per-priority RunFastHandlers/RunHandlers, so the lazy
6960
+ // freeze covers same-priority same-MessageHandler removals correctly.
6961
+ // See the longer rationale on the broadcast inline prefreeze block
6962
+ // in SourcedBroadcast.
6963
+ if (snapshot.bucketCount == 1 && snapshot.buckets[0].entryCount <= 1)
6964
+ {
6965
+ return;
6966
+ }
6967
+
6968
+ DispatchBucket[] buckets = snapshot.buckets;
6969
+ int bucketCount = snapshot.bucketCount;
6970
+ for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
6971
+ {
6972
+ DispatchBucket bucket = buckets[bucketIndex];
6973
+ DispatchEntry[] entries = bucket.entries;
6974
+ int entryCount = bucket.entryCount;
6975
+ if (entryCount == 0)
6976
+ {
6977
+ continue;
6978
+ }
6979
+
6980
+ int priority = bucket.priority;
6981
+ for (int entryIndex = 0; entryIndex < entryCount; ++entryIndex)
6982
+ {
6983
+ entries[entryIndex]
6984
+ .handler.PrefreezeTargetedHandlersForEmission<TMessage>(
6985
+ target,
6986
+ priority,
6987
+ _emissionId,
6988
+ this
6989
+ );
6990
+ }
6991
+ }
6992
+ }
6993
+
4862
6994
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
4863
6995
  private void InvokeGlobalUntargetedEntry<TMessage>(
4864
6996
  ref TMessage message,
@@ -5007,6 +7139,12 @@ namespace DxMessaging.Core.MessageBus
5007
7139
  return;
5008
7140
  }
5009
7141
 
7142
+ // No fast-path short-circuit for post-processor prefreeze. See the
7143
+ // detailed rationale on PrefreezeUntargetedPostSnapshot; a regular
7144
+ // handler can register a new post-processor (same MessageHandler,
7145
+ // same priority) during its own execution, and the lazy first-read
7146
+ // inside post-processor dispatch would otherwise capture that newly
7147
+ // added entry. Always prefreezing pins the emission-start snapshot.
5010
7148
  DispatchBucket[] buckets = snapshot.buckets;
5011
7149
  int bucketCount = snapshot.bucketCount;
5012
7150
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
@@ -5084,6 +7222,12 @@ namespace DxMessaging.Core.MessageBus
5084
7222
  return;
5085
7223
  }
5086
7224
 
7225
+ // No fast-path short-circuit for post-processor prefreeze. See the
7226
+ // detailed rationale on PrefreezeUntargetedPostSnapshot; a regular
7227
+ // handler can register a new post-processor (same MessageHandler,
7228
+ // same priority) during its own execution, and the lazy first-read
7229
+ // inside post-processor dispatch would otherwise capture that newly
7230
+ // added entry. Always prefreezing pins the emission-start snapshot.
5087
7231
  DispatchBucket[] buckets = snapshot.buckets;
5088
7232
  int bucketCount = snapshot.bucketCount;
5089
7233
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)
@@ -5174,6 +7318,13 @@ namespace DxMessaging.Core.MessageBus
5174
7318
  return;
5175
7319
  }
5176
7320
 
7321
+ // No fast-path short-circuit for post-processor prefreeze. See the
7322
+ // detailed rationale on PrefreezeUntargetedPostSnapshot; a regular
7323
+ // handler can register a new post-processor (same MessageHandler,
7324
+ // same priority) during its own execution, and the lazy first-read
7325
+ // inside post-processor dispatch would otherwise capture that newly
7326
+ // added entry. Always prefreezing pins the emission-start snapshot.
7327
+
5177
7328
  DispatchBucket[] buckets = snapshot.buckets;
5178
7329
  int bucketCount = snapshot.bucketCount;
5179
7330
  for (int bucketIndex = 0; bucketIndex < bucketCount; ++bucketIndex)