com.wallstop-studios.dxmessaging 3.1.0 → 3.2.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 (139) hide show
  1. package/CHANGELOG.md +326 -0
  2. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +3 -3
  3. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +3 -3
  4. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +8 -8
  5. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +3 -3
  6. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +171 -13
  7. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +3 -3
  8. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +391 -96
  9. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +3 -3
  10. package/Editor/CustomEditors/MessageAwareComponentInspectorView.cs +279 -0
  11. package/Editor/CustomEditors/MessageAwareComponentInspectorView.cs.meta +11 -0
  12. package/Editor/DxMessagingEditorPalette.cs +66 -0
  13. package/Editor/DxMessagingEditorPalette.cs.meta +11 -0
  14. package/Editor/DxMessagingEditorTheme.cs +200 -0
  15. package/Editor/DxMessagingEditorTheme.cs.meta +11 -0
  16. package/Editor/DxMessagingMenu.cs.meta +3 -3
  17. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +3 -3
  18. package/Editor/Icons/dxmessaging-icon-256.png +0 -0
  19. package/Editor/Icons/dxmessaging-icon-256.png.meta +156 -0
  20. package/Editor/Icons/dxmessaging-icon-32.png +0 -0
  21. package/Editor/Icons/dxmessaging-icon-32.png.meta +156 -0
  22. package/Editor/Icons/dxmessaging-icon-48.png +0 -0
  23. package/Editor/Icons/dxmessaging-icon-48.png.meta +156 -0
  24. package/Editor/Icons.meta +8 -0
  25. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +2 -3
  26. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +3 -3
  27. package/Editor/Settings/DxMessagingSettingsProvider.cs +368 -33
  28. package/Editor/SetupCscRsp.cs +232 -163
  29. package/Editor/Testing/MessagingComponentEditorHarness.cs +11 -4
  30. package/Editor/Theme/DxMessagingTheme.uss +255 -0
  31. package/Editor/Theme/DxMessagingTheme.uss.meta +11 -0
  32. package/Editor/Theme/DxTokens.uss +90 -0
  33. package/Editor/Theme/DxTokens.uss.meta +11 -0
  34. package/Editor/Theme.meta +8 -0
  35. package/Editor/Windows/DxMessagingFlowGraphWindow.cs +5513 -0
  36. package/Editor/Windows/DxMessagingFlowGraphWindow.cs.meta +11 -0
  37. package/Editor/Windows/DxMessagingMessageMonitorWindow.cs +2288 -0
  38. package/Editor/Windows/DxMessagingMessageMonitorWindow.cs.meta +11 -0
  39. package/Editor/Windows.meta +8 -0
  40. package/README.md +84 -35
  41. package/Runtime/Analyzers.meta +8 -0
  42. package/Runtime/AssemblyInfo.cs +1 -0
  43. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +3 -3
  44. package/Runtime/Core/Diagnostics/MessageEmissionData.cs +29 -1
  45. package/Runtime/Core/DxMessagingStaticState.cs.meta +3 -3
  46. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +3 -3
  47. package/Runtime/Core/Extensions/MessageExtensions.cs +4 -5
  48. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +3 -3
  49. package/Runtime/Core/Helper/MessageCache.cs +56 -23
  50. package/Runtime/Core/IMessage.cs.meta +3 -3
  51. package/Runtime/Core/InstanceId.cs.meta +3 -3
  52. package/Runtime/Core/Internal/FlatDispatch.cs +32 -0
  53. package/Runtime/Core/Internal/TypedSlots.cs +45 -7
  54. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +3 -3
  55. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +3 -3
  56. package/Runtime/Core/MessageBus/IMessageBus.cs +63 -28
  57. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +3 -3
  58. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +3 -3
  59. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +3 -3
  60. package/Runtime/Core/MessageBus/MessageBus.cs +1135 -626
  61. package/Runtime/Core/MessageBus/MessageBus.cs.meta +3 -3
  62. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +3 -3
  63. package/Runtime/Core/MessageBus/MessageBusRegistration.cs +207 -0
  64. package/Runtime/Core/MessageBus/MessageBusRegistration.cs.meta +11 -0
  65. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +3 -3
  66. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +3 -3
  67. package/Runtime/Core/MessageBus/RegistrationLog.cs +25 -12
  68. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +3 -3
  69. package/Runtime/Core/MessageHandler.cs +1886 -1082
  70. package/Runtime/Core/MessageHandler.cs.meta +3 -3
  71. package/Runtime/Core/MessageRegistrationHandle.cs +19 -5
  72. package/Runtime/Core/MessageRegistrationHandle.cs.meta +3 -3
  73. package/Runtime/Core/MessageRegistrationToken.cs +1932 -895
  74. package/Runtime/Core/MessageRegistrationToken.cs.meta +3 -3
  75. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +3 -3
  76. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +3 -3
  77. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +3 -3
  78. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +3 -3
  79. package/Runtime/Core/MessagingDebug.cs.meta +3 -3
  80. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +3 -3
  81. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +3 -3
  82. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +3 -3
  83. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +3 -3
  84. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +3 -3
  85. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +3 -3
  86. package/Runtime/Unity/MessageAwareComponent.cs.meta +3 -3
  87. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +3 -3
  88. package/Runtime/Unity/MessagingComponent.cs.meta +3 -3
  89. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +3 -3
  90. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +3 -3
  91. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +3 -3
  92. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +3 -3
  93. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +3 -3
  94. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.cs +173 -0
  95. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.cs.meta +11 -0
  96. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.unity +424 -0
  97. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.unity.meta +7 -0
  98. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingReceiver.cs +185 -0
  99. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingReceiver.cs.meta +11 -0
  100. package/Samples~/Diagnostics Tooling Exerciser/Messages.cs +46 -0
  101. package/Samples~/Diagnostics Tooling Exerciser/Messages.cs.meta +11 -0
  102. package/Samples~/Diagnostics Tooling Exerciser/README.md +53 -0
  103. package/Samples~/Diagnostics Tooling Exerciser/README.md.meta +7 -0
  104. package/Samples~/Diagnostics Tooling Exerciser/WallstopStudios.DxMessaging.DiagnosticsToolingExerciser.Sample.asmdef +13 -0
  105. package/Samples~/Diagnostics Tooling Exerciser/WallstopStudios.DxMessaging.DiagnosticsToolingExerciser.Sample.asmdef.meta +7 -0
  106. package/Samples~/Diagnostics Tooling Exerciser.meta +8 -0
  107. package/Samples~/Mini Combat/Boot.cs.meta +3 -3
  108. package/Samples~/Mini Combat/Enemy.cs.meta +3 -3
  109. package/Samples~/Mini Combat/Messages.cs.meta +3 -3
  110. package/Samples~/Mini Combat/Player.cs.meta +3 -3
  111. package/Samples~/Mini Combat/README.md +1 -1
  112. package/Samples~/Mini Combat/UIOverlay.cs.meta +3 -3
  113. package/Samples~/Mini Combat/Walkthrough.md +3 -3
  114. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +3 -3
  115. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +3 -3
  116. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +3 -3
  117. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +3 -3
  118. package/SourceGenerators/Directory.Build.props +5 -5
  119. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +3 -3
  120. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +12 -11
  121. package/docs/images/DxMessaging-banner.svg +79 -0
  122. package/docs/images/DxMessaging-banner.svg.meta +53 -0
  123. package/docs/images.meta +8 -0
  124. package/docs.meta +8 -0
  125. package/package.json +17 -5
  126. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  127. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +0 -0
  128. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
  129. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll.meta +0 -0
  130. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll +0 -0
  131. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll.meta +0 -0
  132. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll +0 -0
  133. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll.meta +0 -0
  134. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  135. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +0 -0
  136. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  137. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +0 -0
  138. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  139. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +0 -0
@@ -59,6 +59,7 @@ namespace DxMessaging.Core.MessageBus
59
59
  /// </summary>
60
60
  public long EmissionId => 0 < _dispatchDepth ? _scopedEmissionId : _emissionId;
61
61
  internal long TickCounter => _tickCounter;
62
+ internal bool IsDispatching => _dispatchDepth > 0;
62
63
 
63
64
  private const long DefaultIdleEvictionTicks = 30;
64
65
  private const double DefaultEvictionTickIntervalSeconds = 5d;
@@ -173,18 +174,74 @@ namespace DxMessaging.Core.MessageBus
173
174
 
174
175
  private static CollectionPool<
175
176
  Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
176
- > ContextHandlerByTargetDicts => ContextHandlerByTargetDictPoolHolder.Instance;
177
+ > ContextHandlerByTargetDictsOverride;
178
+
179
+ private static CollectionPool<
180
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
181
+ > ContextHandlerByTargetDicts =>
182
+ ContextHandlerByTargetDictsOverride ?? ContextHandlerByTargetDictPoolHolder.Instance;
183
+
184
+ private static CollectionPool<
185
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
186
+ > CreateContextHandlerByTargetPool(int maxRetained, bool useLru)
187
+ {
188
+ return new CollectionPool<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>>(
189
+ maxRetained,
190
+ useLru,
191
+ factory: static () => CreateFreshContextHandlerMap(),
192
+ onRecycled: static dict => dict.Clear()
193
+ );
194
+ }
195
+
196
+ private static Dictionary<
197
+ InstanceId,
198
+ HandlerCache<int, HandlerCache>
199
+ > CreateFreshContextHandlerMap()
200
+ {
201
+ return new Dictionary<InstanceId, HandlerCache<int, HandlerCache>>();
202
+ }
203
+
204
+ internal static object CreateContextMapSeedForBenchmark()
205
+ {
206
+ return new HandlerCache<int, HandlerCache>();
207
+ }
208
+
209
+ internal static object CreatePopulatedContextMapForBenchmark(InstanceId[] keys, object seed)
210
+ {
211
+ if (keys == null)
212
+ {
213
+ throw new ArgumentNullException(nameof(keys));
214
+ }
215
+ HandlerCache<int, HandlerCache> typedSeed =
216
+ seed as HandlerCache<int, HandlerCache>
217
+ ?? throw new ArgumentException("Unexpected context-map seed type.", nameof(seed));
218
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> map =
219
+ CreateFreshContextHandlerMap();
220
+ for (int index = 0; index < keys.Length; ++index)
221
+ {
222
+ map[keys[index]] = typedSeed;
223
+ }
224
+ return map;
225
+ }
226
+
227
+ internal static void ObserveContextMapForBenchmark(
228
+ object opaqueMap,
229
+ out int count,
230
+ out int capacity
231
+ )
232
+ {
233
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> map =
234
+ opaqueMap as Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
235
+ ?? throw new ArgumentException("Unexpected context-map type.", nameof(opaqueMap));
236
+ count = map.Count;
237
+ capacity = map.EnsureCapacity(0);
238
+ }
177
239
 
178
240
  private static class ContextHandlerByTargetDictPoolHolder
179
241
  {
180
242
  public static readonly CollectionPool<
181
243
  Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
182
- > Instance = new(
183
- maxRetained: 512,
184
- useLru: true,
185
- factory: static () => new Dictionary<InstanceId, HandlerCache<int, HandlerCache>>(),
186
- onRecycled: static dict => dict.Clear()
187
- );
244
+ > Instance = CreateContextHandlerByTargetPool(maxRetained: 512, useLru: true);
188
245
  }
189
246
 
190
247
  internal static int ResetStaticPools()
@@ -192,11 +249,10 @@ namespace DxMessaging.Core.MessageBus
192
249
  return ContextHandlerByTargetDicts.Trim(0);
193
250
  }
194
251
 
195
- // One bucket entry per registered MessageHandler. Entry arrays are
196
- // populated only for the GLOBAL accept-all snapshots (the only
197
- // remaining bucket-walking dispatch path); non-global snapshots carry
198
- // count-only buckets (see BuildDispatchSnapshot) because their
199
- // dispatch happens exclusively through the resolved flat arrays.
252
+ // One bucket entry per registered MessageHandler. Bucket arrays are
253
+ // populated only for GLOBAL accept-all snapshots (the only remaining
254
+ // bucket-walking dispatch path). Non-global snapshots carry a resolved
255
+ // flat array and its entry count directly.
200
256
  internal readonly struct DispatchEntry
201
257
  {
202
258
  public DispatchEntry(MessageHandler handler)
@@ -227,11 +283,6 @@ namespace DxMessaging.Core.MessageBus
227
283
  public int entryCount;
228
284
  public bool pooledEntries;
229
285
 
230
- public static DispatchBucket CreateEmpty(int priority)
231
- {
232
- return new DispatchBucket(priority, Array.Empty<DispatchEntry>(), 0, false);
233
- }
234
-
235
286
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
236
287
  public void ReleaseEntries()
237
288
  {
@@ -253,18 +304,42 @@ namespace DxMessaging.Core.MessageBus
253
304
  public static readonly DispatchSnapshot Empty = new DispatchSnapshot(
254
305
  Array.Empty<DispatchBucket>(),
255
306
  0,
307
+ 0,
308
+ false,
256
309
  false
257
310
  );
258
311
 
259
- public DispatchSnapshot(DispatchBucket[] buckets, int count, bool pooled)
312
+ public DispatchSnapshot(
313
+ DispatchBucket[] buckets,
314
+ int bucketCount,
315
+ int entryCount,
316
+ bool hasRegistrations,
317
+ bool pooled
318
+ )
260
319
  {
261
320
  this.buckets = buckets;
262
- bucketCount = count;
321
+ this.bucketCount = bucketCount;
322
+ this.entryCount = entryCount;
323
+ this.hasRegistrations = hasRegistrations;
263
324
  _pooled = pooled;
325
+ _pooledBuckets = pooled;
326
+ }
327
+
328
+ public DispatchSnapshot(FlatDispatchArray flat, bool hasRegistrations)
329
+ {
330
+ buckets = Array.Empty<DispatchBucket>();
331
+ bucketCount = 0;
332
+ entryCount = flat?.Count ?? 0;
333
+ this.hasRegistrations = hasRegistrations;
334
+ this.flat = flat;
335
+ _pooled = true;
336
+ _pooledBuckets = false;
264
337
  }
265
338
 
266
339
  public DispatchBucket[] buckets;
267
340
  public int bucketCount;
341
+ public int entryCount;
342
+ public bool hasRegistrations;
268
343
 
269
344
  // Resolved flat dispatch entries for every flattened slot kind:
270
345
  // untargeted handle/post, targeted/broadcast Default handle/post
@@ -278,8 +353,9 @@ namespace DxMessaging.Core.MessageBus
278
353
  // Release().
279
354
  public FlatDispatchArray flat;
280
355
  private bool _pooled;
356
+ private readonly bool _pooledBuckets;
281
357
 
282
- public bool IsEmpty => bucketCount == 0;
358
+ public bool IsInitialized => !ReferenceEquals(this, Empty);
283
359
 
284
360
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
285
361
  public void Release()
@@ -292,15 +368,21 @@ namespace DxMessaging.Core.MessageBus
292
368
  flat?.Release();
293
369
  flat = null;
294
370
 
295
- for (int i = 0; i < bucketCount; ++i)
371
+ if (_pooledBuckets)
296
372
  {
297
- buckets[i].ReleaseEntries();
373
+ for (int i = 0; i < bucketCount; ++i)
374
+ {
375
+ buckets[i].ReleaseEntries();
376
+ }
377
+
378
+ Array.Clear(buckets, 0, bucketCount);
379
+ DispatchBucketPool.Return(buckets);
298
380
  }
299
381
 
300
- Array.Clear(buckets, 0, bucketCount);
301
- DispatchBucketPool.Return(buckets);
302
382
  buckets = Array.Empty<DispatchBucket>();
303
383
  bucketCount = 0;
384
+ entryCount = 0;
385
+ hasRegistrations = false;
304
386
  _pooled = false;
305
387
  }
306
388
  }
@@ -375,15 +457,12 @@ namespace DxMessaging.Core.MessageBus
375
457
  {
376
458
  public readonly Dictionary<TKey, TValue> handlers = new();
377
459
  public readonly List<TKey> order = new();
378
- public readonly List<KeyValuePair<TKey, TValue>> cache = new();
379
460
  public long version;
380
- public long lastSeenVersion = -1;
381
- public long lastSeenEmissionId = -1;
382
461
  public long lastTouchTicks;
383
462
  public DispatchState dispatchState;
384
463
 
385
464
  /// <summary>
386
- /// Clears all cached handler references and resets the version tracking metadata.
465
+ /// Clears all handler references and resets the mutation version.
387
466
  /// </summary>
388
467
  public void Clear()
389
468
  {
@@ -392,10 +471,7 @@ namespace DxMessaging.Core.MessageBus
392
471
  // is handled by sweep-driven slot reset paths.
393
472
  handlers.Clear();
394
473
  order.Clear();
395
- cache.Clear();
396
474
  version = 0;
397
- lastSeenVersion = -1;
398
- lastSeenEmissionId = -1;
399
475
  dispatchState?.Reset();
400
476
  dispatchState = null;
401
477
  }
@@ -404,13 +480,11 @@ namespace DxMessaging.Core.MessageBus
404
480
  private sealed class InterceptorCache<TValue>
405
481
  {
406
482
  public readonly SortedList<int, List<TValue>> handlers = new();
407
- public long lastSeenEmissionId = -1;
408
483
  public long lastTouchTicks;
409
484
 
410
485
  public void Clear()
411
486
  {
412
487
  handlers.Clear();
413
- lastSeenEmissionId = -1;
414
488
  lastTouchTicks = 0;
415
489
  }
416
490
  }
@@ -489,16 +563,14 @@ namespace DxMessaging.Core.MessageBus
489
563
  // across components. Invariants: contains exactly the keys of
490
564
  // <see cref="handlers"/>; a key is appended on its FIRST
491
565
  // registration only (refcount increments do not move it) and
492
- // removed when its refcount drops to zero. Mirrors the
493
- // MessageHandler-side HandlerActionCache.insertionOrder design.
566
+ // removed when its refcount drops to zero. The MessageHandler-side
567
+ // cache keeps this invariant inside one ordered container; the
568
+ // bus-side map remains a separate candidate.
494
569
  public readonly List<MessageHandler> insertionOrder = new();
495
- public readonly List<MessageHandler> cache = new();
496
570
  public long version;
497
- public long lastSeenVersion = -1;
498
- public long lastSeenEmissionId = -1;
499
571
 
500
572
  /// <summary>
501
- /// Clears all cached handler references and resets the version tracking metadata.
573
+ /// Clears all handler references and resets the mutation version.
502
574
  /// </summary>
503
575
  public void Clear()
504
576
  {
@@ -507,10 +579,7 @@ namespace DxMessaging.Core.MessageBus
507
579
  // is handled by sweep-driven slot reset paths.
508
580
  handlers.Clear();
509
581
  insertionOrder.Clear();
510
- cache.Clear();
511
582
  version = 0;
512
- lastSeenVersion = -1;
513
- lastSeenEmissionId = -1;
514
583
  }
515
584
  }
516
585
 
@@ -784,7 +853,7 @@ namespace DxMessaging.Core.MessageBus
784
853
  public static bool SourcedRegistered;
785
854
  }
786
855
 
787
- public RegistrationLog Log => _log;
856
+ public RegistrationLog Log => _log ??= new RegistrationLog();
788
857
 
789
858
  // Storage trio for typed and global dispatch. _scalarSinks and
790
859
  // _contextSinks are SlotKey-indexed arrays of MessageCache (call sites
@@ -829,6 +898,127 @@ namespace DxMessaging.Core.MessageBus
829
898
  private readonly MessageCache<DispatchPlan> _targetedDispatchPlans = new();
830
899
  private readonly MessageCache<DispatchPlan> _broadcastDispatchPlans = new();
831
900
 
901
+ internal readonly struct FlatSnapshotStorageObservation
902
+ {
903
+ internal FlatSnapshotStorageObservation(
904
+ int entryCount,
905
+ int arrayCapacity,
906
+ int emptyHolderPoolCount
907
+ )
908
+ {
909
+ EntryCount = entryCount;
910
+ ArrayCapacity = arrayCapacity;
911
+ EmptyHolderPoolCount = emptyHolderPoolCount;
912
+ }
913
+
914
+ internal int EntryCount { get; }
915
+
916
+ internal int ArrayCapacity { get; }
917
+
918
+ /// <summary>
919
+ /// Released holder objects currently pooled with empty entry arrays. This does not
920
+ /// measure arrays or retained bytes; ArrayPool memory retention requires an external
921
+ /// memory measurement.
922
+ /// </summary>
923
+ internal int EmptyHolderPoolCount { get; }
924
+ }
925
+
926
+ internal readonly struct PriorityStorageObservation
927
+ {
928
+ internal PriorityStorageObservation(int entries, int mapCapacity, int orderCapacity)
929
+ {
930
+ Entries = entries;
931
+ MapCapacity = mapCapacity;
932
+ OrderCapacity = orderCapacity;
933
+ }
934
+
935
+ internal int Entries { get; }
936
+
937
+ internal int MapCapacity { get; }
938
+
939
+ internal int OrderCapacity { get; }
940
+ }
941
+
942
+ internal bool TryObserveUntargetedPriorityStorageForBenchmark<TMessage>(
943
+ out PriorityStorageObservation observation
944
+ )
945
+ where TMessage : IUntargetedMessage
946
+ {
947
+ if (
948
+ !_scalarSinks[BusSinkIndex.UntargetedHandleDefault]
949
+ .TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> handlers)
950
+ )
951
+ {
952
+ observation = default;
953
+ return false;
954
+ }
955
+
956
+ observation = ObservePriorityStorage(handlers);
957
+ return true;
958
+ }
959
+
960
+ internal static object CreatePriorityStorageOwnerForBenchmark()
961
+ {
962
+ return new HandlerCache<int, HandlerCache>();
963
+ }
964
+
965
+ internal static bool TryObservePriorityStorageOwnerForBenchmark(
966
+ object owner,
967
+ out PriorityStorageObservation observation
968
+ )
969
+ {
970
+ if (owner is not HandlerCache<int, HandlerCache> handlers)
971
+ {
972
+ observation = default;
973
+ return false;
974
+ }
975
+
976
+ observation = ObservePriorityStorage(handlers);
977
+ return true;
978
+ }
979
+
980
+ private static PriorityStorageObservation ObservePriorityStorage(
981
+ HandlerCache<int, HandlerCache> handlers
982
+ )
983
+ {
984
+ return new PriorityStorageObservation(
985
+ handlers.handlers.Count,
986
+ handlers.handlers.EnsureCapacity(0),
987
+ handlers.order.Capacity
988
+ );
989
+ }
990
+
991
+ /// <summary>
992
+ /// Read-only benchmark telemetry for the active untargeted flat snapshot of
993
+ /// <typeparamref name="TMessage"/>. This method does not acquire, build, or mutate a
994
+ /// snapshot, so observing storage cannot alter the path being measured.
995
+ /// </summary>
996
+ internal bool TryObserveUntargetedFlatSnapshotStorageForBenchmark<TMessage>(
997
+ out FlatSnapshotStorageObservation observation
998
+ )
999
+ where TMessage : IUntargetedMessage
1000
+ {
1001
+ if (
1002
+ !_scalarSinks[BusSinkIndex.UntargetedHandleDefault]
1003
+ .TryGetValue<TMessage>(out HandlerCache<int, HandlerCache> handlers)
1004
+ || handlers.dispatchState == null
1005
+ || ReferenceEquals(handlers.dispatchState.active, DispatchSnapshot.Empty)
1006
+ || handlers.dispatchState.active.flat == null
1007
+ )
1008
+ {
1009
+ observation = default;
1010
+ return false;
1011
+ }
1012
+
1013
+ FlatDispatchArray flat = handlers.dispatchState.active.flat;
1014
+ observation = new FlatSnapshotStorageObservation(
1015
+ flat.Count,
1016
+ flat.Capacity,
1017
+ flat.EmptyHolderPoolCount
1018
+ );
1019
+ return true;
1020
+ }
1021
+
832
1022
  // Bumped by every mutation that can change what a DispatchPlan
833
1023
  // caches or decides. Plans compare their stamp against this value at
834
1024
  // every emission; the emit shells also re-compare mid-emission to
@@ -923,7 +1113,9 @@ namespace DxMessaging.Core.MessageBus
923
1113
  }
924
1114
 
925
1115
  #if UNITY_2021_3_OR_NEWER
926
- private static readonly List<WeakReference<MessageBus>> IdleSweepBuses = new();
1116
+ private static List<WeakReference<MessageBus>> IdleSweepBuses = new();
1117
+ private static IdleSweepRegistryBenchmarkScope ActiveIdleSweepRegistryBenchmarkScope;
1118
+ private static ContextMapPoolBenchmarkScope ActiveContextMapPoolBenchmarkScope;
927
1119
  private static bool RuntimeSettingsSubscribed;
928
1120
 
929
1121
  private static void RegisterForIdleSweeps(MessageBus bus)
@@ -997,6 +1189,206 @@ namespace DxMessaging.Core.MessageBus
997
1189
  ResetStaticPools();
998
1190
  }
999
1191
 
1192
+ internal static IDisposable IsolateIdleSweepRegistryForBenchmark()
1193
+ {
1194
+ return new IdleSweepRegistryBenchmarkScope();
1195
+ }
1196
+
1197
+ internal static int IdleSweepRegistryCountForBenchmark => IdleSweepBuses.Count;
1198
+
1199
+ internal static object IdleSweepRegistryIdentityForBenchmark => IdleSweepBuses;
1200
+
1201
+ internal static IDisposable IsolateContextMapPoolForBenchmark()
1202
+ {
1203
+ return new ContextMapPoolBenchmarkScope();
1204
+ }
1205
+
1206
+ internal static bool ContextMapPoolOverrideActiveForBenchmark =>
1207
+ ContextHandlerByTargetDictsOverride != null;
1208
+
1209
+ internal static ContextMapPoolBenchmarkObservation ObserveContextMapPoolForBenchmark()
1210
+ {
1211
+ CollectionPool<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> current =
1212
+ ContextHandlerByTargetDicts;
1213
+ return new ContextMapPoolBenchmarkObservation(
1214
+ current,
1215
+ current.UseLru,
1216
+ current.MaxRetained
1217
+ );
1218
+ }
1219
+
1220
+ internal static void ConfigureContextMapPoolForBenchmark(bool useLru, int maxRetained)
1221
+ {
1222
+ ContextHandlerByTargetDicts.UseLru = useLru;
1223
+ ContextHandlerByTargetDicts.MaxRetained = maxRetained;
1224
+ }
1225
+
1226
+ private sealed class IdleSweepRegistryBenchmarkScope : IDisposable
1227
+ {
1228
+ private readonly List<WeakReference<MessageBus>> _saved;
1229
+ private readonly IdleSweepRegistryBenchmarkScope _parent;
1230
+ private readonly int _ownerThreadId;
1231
+ private bool _disposed;
1232
+
1233
+ internal IdleSweepRegistryBenchmarkScope()
1234
+ {
1235
+ _ownerThreadId = Environment.CurrentManagedThreadId;
1236
+ lock (typeof(IdleSweepRegistryBenchmarkScope))
1237
+ {
1238
+ if (
1239
+ ActiveIdleSweepRegistryBenchmarkScope != null
1240
+ && ActiveIdleSweepRegistryBenchmarkScope._ownerThreadId != _ownerThreadId
1241
+ )
1242
+ {
1243
+ throw new InvalidOperationException(
1244
+ "Nested idle-sweep benchmark scopes must share one owning thread."
1245
+ );
1246
+ }
1247
+
1248
+ _parent = ActiveIdleSweepRegistryBenchmarkScope;
1249
+ _saved = IdleSweepBuses;
1250
+ ActiveIdleSweepRegistryBenchmarkScope = this;
1251
+ IdleSweepBuses = new List<WeakReference<MessageBus>>();
1252
+ }
1253
+ }
1254
+
1255
+ public void Dispose()
1256
+ {
1257
+ lock (typeof(IdleSweepRegistryBenchmarkScope))
1258
+ {
1259
+ if (_disposed)
1260
+ {
1261
+ return;
1262
+ }
1263
+ if (Environment.CurrentManagedThreadId != _ownerThreadId)
1264
+ {
1265
+ throw new InvalidOperationException(
1266
+ "Idle-sweep benchmark scopes must be disposed on their owning thread."
1267
+ );
1268
+ }
1269
+
1270
+ _disposed = true;
1271
+ while (
1272
+ ActiveIdleSweepRegistryBenchmarkScope != null
1273
+ && ActiveIdleSweepRegistryBenchmarkScope._disposed
1274
+ )
1275
+ {
1276
+ IdleSweepRegistryBenchmarkScope completed =
1277
+ ActiveIdleSweepRegistryBenchmarkScope;
1278
+ IdleSweepBuses = completed._saved;
1279
+ ActiveIdleSweepRegistryBenchmarkScope = completed._parent;
1280
+ }
1281
+ }
1282
+ }
1283
+ }
1284
+
1285
+ private sealed class ContextMapPoolBenchmarkScope : IDisposable
1286
+ {
1287
+ private readonly CollectionPool<
1288
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
1289
+ > _savedOverride;
1290
+ private readonly CollectionPool<
1291
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
1292
+ > _savedPool;
1293
+ private readonly CollectionPool<
1294
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
1295
+ > _isolated;
1296
+ private readonly ContextMapPoolBenchmarkScope _parent;
1297
+ private readonly int _ownerThreadId;
1298
+ private readonly bool _initialUseLru;
1299
+ private readonly int _initialMaxRetained;
1300
+ private bool _disposed;
1301
+
1302
+ internal ContextMapPoolBenchmarkScope()
1303
+ {
1304
+ _ownerThreadId = Environment.CurrentManagedThreadId;
1305
+ lock (typeof(ContextMapPoolBenchmarkScope))
1306
+ {
1307
+ if (
1308
+ ActiveContextMapPoolBenchmarkScope != null
1309
+ && ActiveContextMapPoolBenchmarkScope._ownerThreadId != _ownerThreadId
1310
+ )
1311
+ {
1312
+ throw new InvalidOperationException(
1313
+ "Nested context-map benchmark scopes must share one owning thread."
1314
+ );
1315
+ }
1316
+
1317
+ _parent = ActiveContextMapPoolBenchmarkScope;
1318
+ _savedOverride = ContextHandlerByTargetDictsOverride;
1319
+ _savedPool = ContextHandlerByTargetDicts;
1320
+ _initialUseLru = _savedPool.UseLru;
1321
+ _initialMaxRetained = _savedPool.MaxRetained;
1322
+ _isolated = CreateContextHandlerByTargetPool(
1323
+ _initialMaxRetained,
1324
+ _initialUseLru
1325
+ );
1326
+ ActiveContextMapPoolBenchmarkScope = this;
1327
+ ContextHandlerByTargetDictsOverride = _isolated;
1328
+ }
1329
+ }
1330
+
1331
+ public void Dispose()
1332
+ {
1333
+ lock (typeof(ContextMapPoolBenchmarkScope))
1334
+ {
1335
+ if (_disposed)
1336
+ {
1337
+ return;
1338
+ }
1339
+
1340
+ if (Environment.CurrentManagedThreadId != _ownerThreadId)
1341
+ {
1342
+ throw new InvalidOperationException(
1343
+ "Context-map benchmark scopes must be disposed on their owning thread."
1344
+ );
1345
+ }
1346
+ if (!ReferenceEquals(ActiveContextMapPoolBenchmarkScope, this))
1347
+ {
1348
+ throw new InvalidOperationException(
1349
+ "Context-map benchmark scopes must be disposed in strict LIFO order."
1350
+ );
1351
+ }
1352
+
1353
+ bool effectiveUseLru = _isolated.UseLru;
1354
+ int effectiveMaxRetained = _isolated.MaxRetained;
1355
+ if (effectiveUseLru != _initialUseLru)
1356
+ {
1357
+ _savedPool.UseLru = effectiveUseLru;
1358
+ }
1359
+ if (effectiveMaxRetained != _initialMaxRetained)
1360
+ {
1361
+ _savedPool.MaxRetained = effectiveMaxRetained;
1362
+ }
1363
+
1364
+ _disposed = true;
1365
+ _ = _isolated.Trim(0);
1366
+ ContextHandlerByTargetDictsOverride = _savedOverride;
1367
+ ActiveContextMapPoolBenchmarkScope = _parent;
1368
+ }
1369
+ }
1370
+ }
1371
+
1372
+ internal readonly struct ContextMapPoolBenchmarkObservation
1373
+ {
1374
+ internal ContextMapPoolBenchmarkObservation(
1375
+ object identity,
1376
+ bool useLru,
1377
+ int maxRetained
1378
+ )
1379
+ {
1380
+ Identity = identity;
1381
+ UseLru = useLru;
1382
+ MaxRetained = maxRetained;
1383
+ }
1384
+
1385
+ internal object Identity { get; }
1386
+
1387
+ internal bool UseLru { get; }
1388
+
1389
+ internal int MaxRetained { get; }
1390
+ }
1391
+
1000
1392
  private void ApplyRuntimeSettings(DxMessagingRuntimeSettings settings)
1001
1393
  {
1002
1394
  if (settings == null)
@@ -1011,7 +1403,8 @@ namespace DxMessaging.Core.MessageBus
1011
1403
  {
1012
1404
  IMessageBus.GlobalMessageBufferSize = Math.Max(0, settings.MessageBufferSize);
1013
1405
  }
1014
- _emissionBuffer.Resize(Math.Max(0, IMessageBus.GlobalMessageBufferSize));
1406
+ _emissionBufferCapacity = Math.Max(0, IMessageBus.GlobalMessageBufferSize);
1407
+ _emissionBufferBacking?.Resize(_emissionBufferCapacity);
1015
1408
  _idleEvictionTicks = ComputeIdleEvictionTicks(settings.IdleEvictionSeconds);
1016
1409
  _evictionTickIntervalSeconds = Math.Max(0d, settings.EvictionTickIntervalSeconds);
1017
1410
  _idleEvictionEnabled = settings.EvictionEnabled;
@@ -1169,10 +1562,13 @@ namespace DxMessaging.Core.MessageBus
1169
1562
  private readonly List<MonoBehaviour> _componentCache = new();
1170
1563
  #endif
1171
1564
 
1172
- private readonly RegistrationLog _log = new();
1173
- internal readonly CyclicBuffer<MessageEmissionData> _emissionBuffer = new(
1174
- GlobalMessageBufferSize
1175
- );
1565
+ private RegistrationLog _log;
1566
+ private CyclicBuffer<MessageEmissionData> _emissionBufferBacking;
1567
+ private int _emissionBufferCapacity = GlobalMessageBufferSize;
1568
+ internal CyclicBuffer<MessageEmissionData> _emissionBuffer =>
1569
+ _emissionBufferBacking ??= new CyclicBuffer<MessageEmissionData>(
1570
+ _emissionBufferCapacity
1571
+ );
1176
1572
 
1177
1573
  private bool _diagnosticsMode = ShouldEnableDiagnostics();
1178
1574
  private bool _loggedReflexiveWarning;
@@ -1696,7 +2092,7 @@ namespace DxMessaging.Core.MessageBus
1696
2092
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
1697
2093
  private bool HasActiveDispatchSnapshot(DispatchState state)
1698
2094
  {
1699
- return _dispatchDepth > 0 && state != null && !state.active.IsEmpty;
2095
+ return _dispatchDepth > 0 && state != null && state.active.IsInitialized;
1700
2096
  }
1701
2097
 
1702
2098
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -1966,24 +2362,105 @@ namespace DxMessaging.Core.MessageBus
1966
2362
  return ContextHandlerByTargetDicts.Snapshot();
1967
2363
  }
1968
2364
 
1969
- private Dictionary<InstanceId, HandlerCache<int, HandlerCache>> GetOrRentContextMap<T>(
1970
- MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks
1971
- )
1972
- where T : IMessage
2365
+ internal int SweepDirtyTargetSlotForBenchmark<TMessage>(InstanceId target)
2366
+ where TMessage : IMessage
1973
2367
  {
1974
- if (
1975
- sinks.TryGetValue<T>(
1976
- out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
2368
+ int typeIndex = MessageHelperIndexer<TMessage>.SequentialId;
2369
+ int evicted = 0;
2370
+ for (int sinkIndex = 0; sinkIndex < _contextSinks.Length; ++sinkIndex)
2371
+ {
2372
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sink =
2373
+ _contextSinks[sinkIndex];
2374
+ if (
2375
+ sink == null
2376
+ || !sink.TryGetValueAtIndex(
2377
+ typeIndex,
2378
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
2379
+ )
2380
+ || !handlersByTarget.TryGetValue(
2381
+ target,
2382
+ out HandlerCache<int, HandlerCache> handlers
2383
+ )
2384
+ || handlers.handlers.Count != 0
2385
+ || HasActiveDispatchSnapshot(handlers.dispatchState)
1977
2386
  )
2387
+ {
2388
+ continue;
2389
+ }
2390
+
2391
+ handlers.Clear();
2392
+ _ = handlersByTarget.Remove(target);
2393
+ evicted++;
2394
+ }
2395
+
2396
+ if (
2397
+ _dirtyTargets.TryGetValue(typeIndex, out List<InstanceId> targets)
2398
+ && _dirtyTargetSets.TryGetValue(typeIndex, out HashSet<InstanceId> targetSet)
2399
+ && targetSet.Remove(target)
1978
2400
  )
1979
2401
  {
1980
- return handlersByTarget;
2402
+ for (int index = targets.Count - 1; index >= 0; --index)
2403
+ {
2404
+ if (targets[index] != target)
2405
+ {
2406
+ continue;
2407
+ }
2408
+
2409
+ int lastIndex = targets.Count - 1;
2410
+ targets[index] = targets[lastIndex];
2411
+ targets.RemoveAt(lastIndex);
2412
+ break;
2413
+ }
1981
2414
  }
1982
2415
 
1983
- handlersByTarget = ContextHandlerByTargetDicts.Rent();
1984
- _contextMapHighWaterCounts[handlersByTarget] = handlersByTarget.Count;
1985
- sinks.Set<T>(handlersByTarget);
1986
- return handlersByTarget;
2416
+ // Deliberately retain the empty per-type context map and dirty-candidate
2417
+ // collections. Returning and immediately renting them would make this row measure
2418
+ // shared pools in addition to the target map. ResetState returns them at teardown.
2419
+ return evicted;
2420
+ }
2421
+
2422
+ internal bool TryObserveTargetedHandleMapStorageForBenchmark<TMessage>(
2423
+ out int entries,
2424
+ out int capacity
2425
+ )
2426
+ where TMessage : IMessage
2427
+ {
2428
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sink =
2429
+ _contextSinks[BusContextIndex.TargetedHandleDefault];
2430
+ if (
2431
+ !sink.TryGetValue<TMessage>(
2432
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
2433
+ )
2434
+ )
2435
+ {
2436
+ entries = 0;
2437
+ capacity = 0;
2438
+ return false;
2439
+ }
2440
+
2441
+ entries = handlersByTarget.Count;
2442
+ capacity = handlersByTarget.EnsureCapacity(0);
2443
+ return true;
2444
+ }
2445
+
2446
+ private Dictionary<InstanceId, HandlerCache<int, HandlerCache>> GetOrRentContextMap<T>(
2447
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks
2448
+ )
2449
+ where T : IMessage
2450
+ {
2451
+ if (
2452
+ sinks.TryGetValue<T>(
2453
+ out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> handlersByTarget
2454
+ )
2455
+ )
2456
+ {
2457
+ return handlersByTarget;
2458
+ }
2459
+
2460
+ handlersByTarget = ContextHandlerByTargetDicts.Rent();
2461
+ _contextMapHighWaterCounts[handlersByTarget] = handlersByTarget.Count;
2462
+ sinks.Set<T>(handlersByTarget);
2463
+ return handlersByTarget;
1987
2464
  }
1988
2465
 
1989
2466
  private void RemoveAndReturnContextMap(
@@ -2130,7 +2607,7 @@ namespace DxMessaging.Core.MessageBus
2130
2607
  {
2131
2608
  DispatchSnapshot displaced = snapshot;
2132
2609
  snapshot = DispatchSnapshot.Empty;
2133
- if (displaced == null || displaced.IsEmpty)
2610
+ if (displaced == null || !displaced.IsInitialized)
2134
2611
  {
2135
2612
  return;
2136
2613
  }
@@ -2474,11 +2951,10 @@ namespace DxMessaging.Core.MessageBus
2474
2951
  _componentCache.Clear();
2475
2952
  #endif
2476
2953
 
2477
- bool enabled = _log.Enabled;
2478
- _log.Clear();
2479
- _log.Enabled = enabled;
2480
- _emissionBuffer.Resize(GlobalMessageBufferSize);
2481
- _emissionBuffer.Clear();
2954
+ _log?.Clear();
2955
+ _emissionBufferCapacity = GlobalMessageBufferSize;
2956
+ _emissionBufferBacking?.Resize(_emissionBufferCapacity);
2957
+ _emissionBufferBacking?.Clear();
2482
2958
  }
2483
2959
 
2484
2960
  private void ResetTypedSlotsForReferencedHandlers()
@@ -2562,7 +3038,10 @@ namespace DxMessaging.Core.MessageBus
2562
3038
  }
2563
3039
 
2564
3040
  /// <inheritdoc />
2565
- public Action RegisterUntargeted<T>(MessageHandler messageHandler, int priority = 0)
3041
+ public MessageBusRegistration RegisterUntargeted<T>(
3042
+ MessageHandler messageHandler,
3043
+ int priority = 0
3044
+ )
2566
3045
  where T : IUntargetedMessage
2567
3046
  {
2568
3047
  EnsureAotUntargetedBridge<T>();
@@ -2575,7 +3054,7 @@ namespace DxMessaging.Core.MessageBus
2575
3054
  }
2576
3055
 
2577
3056
  /// <inheritdoc />
2578
- public Action RegisterTargeted<T>(
3057
+ public MessageBusRegistration RegisterTargeted<T>(
2579
3058
  InstanceId target,
2580
3059
  MessageHandler messageHandler,
2581
3060
  int priority = 0
@@ -2593,7 +3072,7 @@ namespace DxMessaging.Core.MessageBus
2593
3072
  }
2594
3073
 
2595
3074
  /// <inheritdoc />
2596
- public Action RegisterSourcedBroadcast<T>(
3075
+ public MessageBusRegistration RegisterSourcedBroadcast<T>(
2597
3076
  InstanceId source,
2598
3077
  MessageHandler messageHandler,
2599
3078
  int priority = 0
@@ -2611,7 +3090,7 @@ namespace DxMessaging.Core.MessageBus
2611
3090
  }
2612
3091
 
2613
3092
  /// <inheritdoc />
2614
- public Action RegisterSourcedBroadcastWithoutSource<T>(
3093
+ public MessageBusRegistration RegisterSourcedBroadcastWithoutSource<T>(
2615
3094
  MessageHandler messageHandler,
2616
3095
  int priority = 0
2617
3096
  )
@@ -2627,7 +3106,7 @@ namespace DxMessaging.Core.MessageBus
2627
3106
  }
2628
3107
 
2629
3108
  /// <inheritdoc />
2630
- public Action RegisterTargetedWithoutTargeting<T>(
3109
+ public MessageBusRegistration RegisterTargetedWithoutTargeting<T>(
2631
3110
  MessageHandler messageHandler,
2632
3111
  int priority = 0
2633
3112
  )
@@ -2643,7 +3122,7 @@ namespace DxMessaging.Core.MessageBus
2643
3122
  }
2644
3123
 
2645
3124
  /// <inheritdoc />
2646
- public Action RegisterGlobalAcceptAll(MessageHandler messageHandler)
3125
+ public MessageBusRegistration RegisterGlobalAcceptAll(MessageHandler messageHandler)
2647
3126
  {
2648
3127
  long touchTick = AdvanceTick();
2649
3128
  InvalidateDispatchPlans();
@@ -2661,7 +3140,7 @@ namespace DxMessaging.Core.MessageBus
2661
3140
  {
2662
3141
  _globalSlots.liveCount++;
2663
3142
  }
2664
- _log.Log(
3143
+ _log?.Log(
2665
3144
  new MessagingRegistration(
2666
3145
  messageHandler.owner,
2667
3146
  type,
@@ -2689,88 +3168,107 @@ namespace DxMessaging.Core.MessageBus
2689
3168
 
2690
3169
  long capturedGeneration = _resetGeneration;
2691
3170
  long capturedSweepGeneration = _globalSlotSweepGeneration;
2692
- return () =>
3171
+ return new MessageBusRegistration(
3172
+ MessageBusRegistration.Kind.GlobalAcceptAll,
3173
+ RegistrationMethod.GlobalAcceptAll,
3174
+ 0,
3175
+ capturedGeneration,
3176
+ capturedSweepGeneration,
3177
+ touchTick,
3178
+ null,
3179
+ null,
3180
+ messageHandler,
3181
+ default
3182
+ );
3183
+ }
3184
+
3185
+ /// <summary>
3186
+ /// Re-expression of the former <see cref="RegisterGlobalAcceptAll"/> closure body: the
3187
+ /// global accept-all deregistration. Behaviour and both generation guards are unchanged.
3188
+ /// </summary>
3189
+ private void DeregisterGlobalAcceptAll(in MessageBusRegistration reg)
3190
+ {
3191
+ // Generation guard: see DeregisterScalarHandler. The global slot guards on BOTH the
3192
+ // reset generation and the global-slot sweep generation.
3193
+ if (
3194
+ reg.generation != _resetGeneration
3195
+ || reg.sweepGeneration != _globalSlotSweepGeneration
3196
+ )
2693
3197
  {
2694
- // Generation guard: see InternalRegisterUntargeted for the
2695
- // rationale. Skip silently when the closure outlived a Reset.
2696
- if (
2697
- capturedGeneration != _resetGeneration
2698
- || capturedSweepGeneration != _globalSlotSweepGeneration
3198
+ return;
3199
+ }
3200
+
3201
+ MessageHandler messageHandler = (MessageHandler)reg.payload;
3202
+ Type type = typeof(IMessage);
3203
+
3204
+ long deregisterTouchTick = AdvanceTick();
3205
+ InvalidateDispatchPlans();
3206
+ _globalSlots.version++;
3207
+ _log?.Log(
3208
+ new MessagingRegistration(
3209
+ messageHandler.owner,
3210
+ type,
3211
+ RegistrationType.Deregister,
3212
+ RegistrationMethod.GlobalAcceptAll
2699
3213
  )
3214
+ );
3215
+ if (!_globalSlots.sharedHandlers.TryGetValue(messageHandler, out int count))
3216
+ {
3217
+ if (MessagingDebug.enabled)
2700
3218
  {
2701
- return;
3219
+ MessagingDebug.Log(
3220
+ LogLevel.Error,
3221
+ "Received over-deregistration of GlobalAcceptAll for MessageHandler {0}. Check to make sure you're not calling (de)registration multiple times.",
3222
+ messageHandler
3223
+ );
2702
3224
  }
2703
3225
 
2704
- long deregisterTouchTick = AdvanceTick();
2705
- InvalidateDispatchPlans();
2706
- _globalSlots.version++;
2707
- _log.Log(
2708
- new MessagingRegistration(
2709
- messageHandler.owner,
2710
- type,
2711
- RegistrationType.Deregister,
2712
- RegistrationMethod.GlobalAcceptAll
2713
- )
2714
- );
2715
- if (!_globalSlots.sharedHandlers.TryGetValue(messageHandler, out count))
2716
- {
2717
- if (MessagingDebug.enabled)
2718
- {
2719
- MessagingDebug.Log(
2720
- LogLevel.Error,
2721
- "Received over-deregistration of GlobalAcceptAll for MessageHandler {0}. Check to make sure you're not calling (de)registration multiple times.",
2722
- messageHandler
2723
- );
2724
- }
2725
-
2726
- return;
2727
- }
3226
+ return;
3227
+ }
2728
3228
 
2729
- _globalSlots.lastTouchTicks = deregisterTouchTick;
2730
- if (count <= 1)
2731
- {
2732
- _ = _globalSlots.sharedHandlers.Remove(messageHandler);
2733
- MarkDirtyHandler(messageHandler);
2734
- _globalSlotSweepCandidate = true;
2735
- // Final-removal of this handler from sharedHandlers is the
2736
- // 1 -> 0 transition that mirrors back into liveCount.
2737
- // Partial deregistration (count > 1) leaves liveCount
2738
- // alone -- the dictionary entry is still present.
2739
- _globalSlots.liveCount--;
2740
- }
2741
- else
2742
- {
2743
- _globalSlots.sharedHandlers[messageHandler] = count - 1;
2744
- }
3229
+ _globalSlots.lastTouchTicks = deregisterTouchTick;
3230
+ if (count <= 1)
3231
+ {
3232
+ _ = _globalSlots.sharedHandlers.Remove(messageHandler);
3233
+ MarkDirtyHandler(messageHandler);
3234
+ _globalSlotSweepCandidate = true;
3235
+ // Final-removal of this handler from sharedHandlers is the 1 -> 0 transition that
3236
+ // mirrors back into liveCount. Partial deregistration (count > 1) leaves liveCount
3237
+ // alone -- the dictionary entry is still present.
3238
+ _globalSlots.liveCount--;
3239
+ }
3240
+ else
3241
+ {
3242
+ _globalSlots.sharedHandlers[messageHandler] = count - 1;
3243
+ }
2745
3244
 
2746
- StageGlobalDispatchSnapshot<IUntargetedMessage>(
2747
- this,
2748
- _globalSlots,
2749
- DispatchKind.Untargeted
2750
- );
2751
- StageGlobalDispatchSnapshot<ITargetedMessage>(
2752
- this,
2753
- _globalSlots,
2754
- DispatchKind.Targeted
2755
- );
2756
- StageGlobalDispatchSnapshot<IBroadcastMessage>(
2757
- this,
2758
- _globalSlots,
2759
- DispatchKind.Broadcast
2760
- );
2761
- DebugAssertGlobalLiveCount();
2762
- };
3245
+ StageGlobalDispatchSnapshot<IUntargetedMessage>(
3246
+ this,
3247
+ _globalSlots,
3248
+ DispatchKind.Untargeted
3249
+ );
3250
+ StageGlobalDispatchSnapshot<ITargetedMessage>(
3251
+ this,
3252
+ _globalSlots,
3253
+ DispatchKind.Targeted
3254
+ );
3255
+ StageGlobalDispatchSnapshot<IBroadcastMessage>(
3256
+ this,
3257
+ _globalSlots,
3258
+ DispatchKind.Broadcast
3259
+ );
3260
+ DebugAssertGlobalLiveCount();
2763
3261
  }
2764
3262
 
2765
3263
  /// <inheritdoc />
2766
- public Action RegisterUntargetedInterceptor<T>(
3264
+ public MessageBusRegistration RegisterUntargetedInterceptor<T>(
2767
3265
  UntargetedInterceptor<T> interceptor,
2768
3266
  int priority = 0
2769
3267
  )
2770
3268
  where T : IUntargetedMessage
2771
3269
  {
2772
3270
  EnsureAotUntargetedBridge<T>();
2773
- _ = AdvanceTick();
3271
+ long touchTick = AdvanceTick();
2774
3272
  InvalidateDispatchPlans();
2775
3273
  InterceptorCache<object> prioritizedInterceptors =
2776
3274
  _untargetedInterceptsByType.GetOrAdd<T>();
@@ -2809,7 +3307,7 @@ namespace DxMessaging.Core.MessageBus
2809
3307
  priorityCount[priority] = count + 1;
2810
3308
 
2811
3309
  Type type = typeof(T);
2812
- _log.Log(
3310
+ _log?.Log(
2813
3311
  new MessagingRegistration(
2814
3312
  InstanceId.EmptyId,
2815
3313
  type,
@@ -2819,107 +3317,29 @@ namespace DxMessaging.Core.MessageBus
2819
3317
  );
2820
3318
 
2821
3319
  long capturedGeneration = _resetGeneration;
2822
- return () =>
2823
- {
2824
- // Generation guard: see InternalRegisterUntargeted for the
2825
- // rationale. Skip silently when the closure outlived a Reset.
2826
- if (capturedGeneration != _resetGeneration)
2827
- {
2828
- return;
2829
- }
2830
- if (
2831
- IsStaleInterceptorDeregisterAfterSweep<T>(
2832
- _untargetedInterceptsByType,
2833
- capturedInterceptors
2834
- )
2835
- )
2836
- {
2837
- return;
2838
- }
2839
-
2840
- _ = AdvanceTick();
2841
- InvalidateDispatchPlans();
2842
- prioritizedInterceptors.lastTouchTicks = _tickCounter;
2843
- MarkDirtyType<T>();
2844
- _log.Log(
2845
- new MessagingRegistration(
2846
- InstanceId.EmptyId,
2847
- type,
2848
- RegistrationType.Deregister,
2849
- RegistrationMethod.Interceptor
2850
- )
2851
- );
2852
- bool removed = false;
2853
- if (_uniqueInterceptorsAndPriorities.TryGetValue(interceptor, out priorityCount))
2854
- {
2855
- if (priorityCount.TryGetValue(priority, out count))
2856
- {
2857
- if (1 < count)
2858
- {
2859
- priorityCount[priority] = count - 1;
2860
- }
2861
- else
2862
- {
2863
- removed = true;
2864
- _ = priorityCount.Remove(priority);
2865
- }
2866
- }
2867
-
2868
- if (priorityCount.Count == 0)
2869
- {
2870
- _uniqueInterceptorsAndPriorities.Remove(interceptor);
2871
- }
2872
- }
2873
- else if (MessagingDebug.enabled)
2874
- {
2875
- MessagingDebug.Log(
2876
- LogLevel.Error,
2877
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
2878
- interceptor
2879
- );
2880
- }
2881
-
2882
- bool complete = false;
2883
- if (removed)
2884
- {
2885
- if (_untargetedInterceptsByType.TryGetValue<T>(out prioritizedInterceptors))
2886
- {
2887
- if (
2888
- prioritizedInterceptors.handlers.TryGetValue(
2889
- priority,
2890
- out List<object> interceptors
2891
- )
2892
- )
2893
- {
2894
- complete = interceptors.Remove(interceptor);
2895
- if (interceptors.Count == 0)
2896
- {
2897
- _ = prioritizedInterceptors.handlers.Remove(priority);
2898
- }
2899
- }
2900
- }
2901
-
2902
- if (!complete && MessagingDebug.enabled)
2903
- {
2904
- MessagingDebug.Log(
2905
- LogLevel.Error,
2906
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
2907
- interceptor
2908
- );
2909
- }
2910
- }
2911
- };
3320
+ return new MessageBusRegistration(
3321
+ MessageBusRegistration.Kind.UntargetedInterceptor,
3322
+ RegistrationMethod.Interceptor,
3323
+ priority,
3324
+ capturedGeneration,
3325
+ 0L,
3326
+ touchTick,
3327
+ capturedInterceptors,
3328
+ null,
3329
+ interceptor,
3330
+ default
3331
+ );
2912
3332
  }
2913
3333
 
2914
3334
  /// <inheritdoc />
2915
- public Action RegisterTargetedInterceptor<T>(
3335
+ public MessageBusRegistration RegisterTargetedInterceptor<T>(
2916
3336
  TargetedInterceptor<T> interceptor,
2917
3337
  int priority = 0
2918
3338
  )
2919
3339
  where T : ITargetedMessage
2920
3340
  {
2921
3341
  EnsureAotTargetedBridge<T>();
2922
- _ = AdvanceTick();
3342
+ long touchTick = AdvanceTick();
2923
3343
  InvalidateDispatchPlans();
2924
3344
  InterceptorCache<object> prioritizedInterceptors =
2925
3345
  _targetedInterceptsByType.GetOrAdd<T>();
@@ -2958,7 +3378,7 @@ namespace DxMessaging.Core.MessageBus
2958
3378
  priorityCount[priority] = count + 1;
2959
3379
 
2960
3380
  Type type = typeof(T);
2961
- _log.Log(
3381
+ _log?.Log(
2962
3382
  new MessagingRegistration(
2963
3383
  InstanceId.EmptyId,
2964
3384
  type,
@@ -2968,107 +3388,29 @@ namespace DxMessaging.Core.MessageBus
2968
3388
  );
2969
3389
 
2970
3390
  long capturedGeneration = _resetGeneration;
2971
- return () =>
2972
- {
2973
- // Generation guard: see InternalRegisterUntargeted for the
2974
- // rationale. Skip silently when the closure outlived a Reset.
2975
- if (capturedGeneration != _resetGeneration)
2976
- {
2977
- return;
2978
- }
2979
- if (
2980
- IsStaleInterceptorDeregisterAfterSweep<T>(
2981
- _targetedInterceptsByType,
2982
- capturedInterceptors
2983
- )
2984
- )
2985
- {
2986
- return;
2987
- }
2988
-
2989
- _ = AdvanceTick();
2990
- InvalidateDispatchPlans();
2991
- prioritizedInterceptors.lastTouchTicks = _tickCounter;
2992
- MarkDirtyType<T>();
2993
- _log.Log(
2994
- new MessagingRegistration(
2995
- InstanceId.EmptyId,
2996
- type,
2997
- RegistrationType.Deregister,
2998
- RegistrationMethod.Interceptor
2999
- )
3000
- );
3001
- bool removed = false;
3002
- if (_uniqueInterceptorsAndPriorities.TryGetValue(interceptor, out priorityCount))
3003
- {
3004
- if (priorityCount.TryGetValue(priority, out count))
3005
- {
3006
- if (1 < count)
3007
- {
3008
- priorityCount[priority] = count - 1;
3009
- }
3010
- else
3011
- {
3012
- removed = true;
3013
- _ = priorityCount.Remove(priority);
3014
- }
3015
- }
3016
-
3017
- if (priorityCount.Count == 0)
3018
- {
3019
- _uniqueInterceptorsAndPriorities.Remove(interceptor);
3020
- }
3021
- }
3022
- else if (MessagingDebug.enabled)
3023
- {
3024
- MessagingDebug.Log(
3025
- LogLevel.Error,
3026
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
3027
- interceptor
3028
- );
3029
- }
3030
-
3031
- bool complete = false;
3032
- if (removed)
3033
- {
3034
- if (_targetedInterceptsByType.TryGetValue<T>(out prioritizedInterceptors))
3035
- {
3036
- if (
3037
- prioritizedInterceptors.handlers.TryGetValue(
3038
- priority,
3039
- out List<object> interceptors
3040
- )
3041
- )
3042
- {
3043
- complete = interceptors.Remove(interceptor);
3044
- if (interceptors.Count == 0)
3045
- {
3046
- _ = prioritizedInterceptors.handlers.Remove(priority);
3047
- }
3048
- }
3049
- }
3050
-
3051
- if (!complete && MessagingDebug.enabled)
3052
- {
3053
- MessagingDebug.Log(
3054
- LogLevel.Error,
3055
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
3056
- interceptor
3057
- );
3058
- }
3059
- }
3060
- };
3391
+ return new MessageBusRegistration(
3392
+ MessageBusRegistration.Kind.TargetedInterceptor,
3393
+ RegistrationMethod.Interceptor,
3394
+ priority,
3395
+ capturedGeneration,
3396
+ 0L,
3397
+ touchTick,
3398
+ capturedInterceptors,
3399
+ null,
3400
+ interceptor,
3401
+ default
3402
+ );
3061
3403
  }
3062
3404
 
3063
3405
  /// <inheritdoc />
3064
- public Action RegisterBroadcastInterceptor<T>(
3406
+ public MessageBusRegistration RegisterBroadcastInterceptor<T>(
3065
3407
  BroadcastInterceptor<T> interceptor,
3066
3408
  int priority = 0
3067
3409
  )
3068
3410
  where T : IBroadcastMessage
3069
3411
  {
3070
3412
  EnsureAotSourcedBridge<T>();
3071
- _ = AdvanceTick();
3413
+ long touchTick = AdvanceTick();
3072
3414
  InvalidateDispatchPlans();
3073
3415
  InterceptorCache<object> prioritizedInterceptors =
3074
3416
  _broadcastInterceptsByType.GetOrAdd<T>();
@@ -3107,7 +3449,7 @@ namespace DxMessaging.Core.MessageBus
3107
3449
  priorityCount[priority] = count + 1;
3108
3450
 
3109
3451
  Type type = typeof(T);
3110
- _log.Log(
3452
+ _log?.Log(
3111
3453
  new MessagingRegistration(
3112
3454
  InstanceId.EmptyId,
3113
3455
  type,
@@ -3117,96 +3459,18 @@ namespace DxMessaging.Core.MessageBus
3117
3459
  );
3118
3460
 
3119
3461
  long capturedGeneration = _resetGeneration;
3120
- return () =>
3121
- {
3122
- // Generation guard: see InternalRegisterUntargeted for the
3123
- // rationale. Skip silently when the closure outlived a Reset.
3124
- if (capturedGeneration != _resetGeneration)
3125
- {
3126
- return;
3127
- }
3128
- if (
3129
- IsStaleInterceptorDeregisterAfterSweep<T>(
3130
- _broadcastInterceptsByType,
3131
- capturedInterceptors
3132
- )
3133
- )
3134
- {
3135
- return;
3136
- }
3137
-
3138
- _ = AdvanceTick();
3139
- InvalidateDispatchPlans();
3140
- prioritizedInterceptors.lastTouchTicks = _tickCounter;
3141
- MarkDirtyType<T>();
3142
- _log.Log(
3143
- new MessagingRegistration(
3144
- InstanceId.EmptyId,
3145
- type,
3146
- RegistrationType.Deregister,
3147
- RegistrationMethod.Interceptor
3148
- )
3149
- );
3150
- bool removed = false;
3151
- if (_uniqueInterceptorsAndPriorities.TryGetValue(interceptor, out priorityCount))
3152
- {
3153
- if (priorityCount.TryGetValue(priority, out count))
3154
- {
3155
- if (1 < count)
3156
- {
3157
- priorityCount[priority] = count - 1;
3158
- }
3159
- else
3160
- {
3161
- removed = true;
3162
- _ = priorityCount.Remove(priority);
3163
- }
3164
- }
3165
-
3166
- if (priorityCount.Count == 0)
3167
- {
3168
- _uniqueInterceptorsAndPriorities.Remove(interceptor);
3169
- }
3170
- }
3171
- else if (MessagingDebug.enabled)
3172
- {
3173
- MessagingDebug.Log(
3174
- LogLevel.Error,
3175
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
3176
- interceptor
3177
- );
3178
- }
3179
-
3180
- bool complete = false;
3181
- if (removed)
3182
- {
3183
- if (_broadcastInterceptsByType.TryGetValue<T>(out prioritizedInterceptors))
3184
- {
3185
- if (
3186
- prioritizedInterceptors.handlers.TryGetValue(
3187
- priority,
3188
- out List<object> interceptors
3189
- )
3190
- )
3191
- {
3192
- complete = interceptors.Remove(interceptor);
3193
- if (interceptors.Count == 0)
3194
- {
3195
- _ = prioritizedInterceptors.handlers.Remove(priority);
3196
- }
3197
- }
3198
- }
3199
-
3200
- if (!complete && MessagingDebug.enabled)
3201
- {
3202
- MessagingDebug.Log(
3203
- LogLevel.Error,
3204
- "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
3205
- interceptor
3206
- );
3207
- }
3208
- }
3209
- };
3462
+ return new MessageBusRegistration(
3463
+ MessageBusRegistration.Kind.BroadcastInterceptor,
3464
+ RegistrationMethod.Interceptor,
3465
+ priority,
3466
+ capturedGeneration,
3467
+ 0L,
3468
+ touchTick,
3469
+ capturedInterceptors,
3470
+ null,
3471
+ interceptor,
3472
+ default
3473
+ );
3210
3474
  }
3211
3475
 
3212
3476
  private bool IsStaleInterceptorDeregisterAfterSweep<T>(
@@ -3221,7 +3485,7 @@ namespace DxMessaging.Core.MessageBus
3221
3485
  }
3222
3486
 
3223
3487
  /// <inheritdoc />
3224
- public Action RegisterUntargetedPostProcessor<T>(
3488
+ public MessageBusRegistration RegisterUntargetedPostProcessor<T>(
3225
3489
  MessageHandler messageHandler,
3226
3490
  int priority = 0
3227
3491
  )
@@ -3237,7 +3501,7 @@ namespace DxMessaging.Core.MessageBus
3237
3501
  }
3238
3502
 
3239
3503
  /// <inheritdoc />
3240
- public Action RegisterTargetedPostProcessor<T>(
3504
+ public MessageBusRegistration RegisterTargetedPostProcessor<T>(
3241
3505
  InstanceId target,
3242
3506
  MessageHandler messageHandler,
3243
3507
  int priority = 0
@@ -3255,7 +3519,7 @@ namespace DxMessaging.Core.MessageBus
3255
3519
  }
3256
3520
 
3257
3521
  /// <inheritdoc />
3258
- public Action RegisterTargetedWithoutTargetingPostProcessor<T>(
3522
+ public MessageBusRegistration RegisterTargetedWithoutTargetingPostProcessor<T>(
3259
3523
  MessageHandler messageHandler,
3260
3524
  int priority = 0
3261
3525
  )
@@ -3271,7 +3535,7 @@ namespace DxMessaging.Core.MessageBus
3271
3535
  }
3272
3536
 
3273
3537
  /// <inheritdoc />
3274
- public Action RegisterBroadcastPostProcessor<T>(
3538
+ public MessageBusRegistration RegisterBroadcastPostProcessor<T>(
3275
3539
  InstanceId source,
3276
3540
  MessageHandler messageHandler,
3277
3541
  int priority = 0
@@ -3289,7 +3553,7 @@ namespace DxMessaging.Core.MessageBus
3289
3553
  }
3290
3554
 
3291
3555
  /// <inheritdoc />
3292
- public Action RegisterBroadcastWithoutSourcePostProcessor<T>(
3556
+ public MessageBusRegistration RegisterBroadcastWithoutSourcePostProcessor<T>(
3293
3557
  MessageHandler messageHandler,
3294
3558
  int priority = 0
3295
3559
  )
@@ -3382,7 +3646,7 @@ namespace DxMessaging.Core.MessageBus
3382
3646
  long touchTick = AdvanceTick();
3383
3647
  if (_diagnosticsMode)
3384
3648
  {
3385
- _emissionBuffer.Add(new MessageEmissionData(typedMessage));
3649
+ _emissionBuffer.Add(new MessageEmissionData(typedMessage, emissionId));
3386
3650
  }
3387
3651
 
3388
3652
  if (plan.fastPath)
@@ -3517,7 +3781,7 @@ namespace DxMessaging.Core.MessageBus
3517
3781
  }
3518
3782
 
3519
3783
  Touch(sortedHandlers, touchTick);
3520
- DispatchSnapshot snapshot = prefrozenSnapshot.IsEmpty
3784
+ DispatchSnapshot snapshot = !prefrozenSnapshot.IsInitialized
3521
3785
  ? AcquireDispatchSnapshotFast<TMessage>(
3522
3786
  this,
3523
3787
  sortedHandlers,
@@ -3624,7 +3888,7 @@ namespace DxMessaging.Core.MessageBus
3624
3888
  long touchTick = AdvanceTick();
3625
3889
  if (_diagnosticsMode)
3626
3890
  {
3627
- _emissionBuffer.Add(new MessageEmissionData(typedMessage, target));
3891
+ _emissionBuffer.Add(new MessageEmissionData(typedMessage, target, emissionId));
3628
3892
  }
3629
3893
 
3630
3894
  // Fast lane: no interceptors, no global accept-all, no
@@ -4105,7 +4369,7 @@ namespace DxMessaging.Core.MessageBus
4105
4369
  target
4106
4370
  );
4107
4371
  }
4108
- else if (targetedPostSnapshot.IsEmpty)
4372
+ else if (!targetedPostSnapshot.IsInitialized)
4109
4373
  {
4110
4374
  snapshot = AcquireDispatchSnapshotFast<TMessage>(
4111
4375
  this,
@@ -4131,7 +4395,7 @@ namespace DxMessaging.Core.MessageBus
4131
4395
  && postTwt.handlers.Count > 0
4132
4396
  )
4133
4397
  {
4134
- DispatchSnapshot snapshot = targetedWithoutTargetingPostSnapshot.IsEmpty
4398
+ DispatchSnapshot snapshot = !targetedWithoutTargetingPostSnapshot.IsInitialized
4135
4399
  ? AcquireDispatchSnapshotFast<TMessage>(
4136
4400
  this,
4137
4401
  postTwt,
@@ -4253,7 +4517,7 @@ namespace DxMessaging.Core.MessageBus
4253
4517
  long touchTick = AdvanceTick();
4254
4518
  if (_diagnosticsMode)
4255
4519
  {
4256
- _emissionBuffer.Add(new MessageEmissionData(typedMessage, source));
4520
+ _emissionBuffer.Add(new MessageEmissionData(typedMessage, source, emissionId));
4257
4521
  }
4258
4522
 
4259
4523
  // Fast lane: no interceptors, no global accept-all, no
@@ -4510,15 +4774,15 @@ namespace DxMessaging.Core.MessageBus
4510
4774
  }
4511
4775
  }
4512
4776
 
4513
- if (!broadcastPostSnapshot.IsEmpty)
4777
+ if (broadcastPostSnapshot.IsInitialized)
4514
4778
  {
4515
- // Legacy reporting: a non-empty pre-frozen snapshot counts as
4516
- // "found" even if every frozen delegate is skipped below.
4779
+ // Legacy reporting: an initialized pre-frozen snapshot counts as
4780
+ // "found" even when it owns zero resolved delegates.
4517
4781
  foundAnyHandlers = true;
4518
4782
  _ = DispatchFlatSnapshot(broadcastPostSnapshot, ref typedMessage);
4519
4783
  }
4520
4784
 
4521
- if (!broadcastWithoutSourcePostSnapshot.IsEmpty)
4785
+ if (broadcastWithoutSourcePostSnapshot.IsInitialized)
4522
4786
  {
4523
4787
  if (
4524
4788
  DispatchContextFlatSnapshot(
@@ -5107,7 +5371,7 @@ namespace DxMessaging.Core.MessageBus
5107
5371
  // Empty), acquire lazily here: the legacy path took its first
5108
5372
  // freeze at this point in that case, so a registration made
5109
5373
  // earlier in this emission into a previously-empty sink fires.
5110
- DispatchSnapshot snapshot = prefrozenSnapshot.IsEmpty
5374
+ DispatchSnapshot snapshot = !prefrozenSnapshot.IsInitialized
5111
5375
  ? AcquireDispatchSnapshotFast<TMessage>(
5112
5376
  this,
5113
5377
  sortedHandlers,
@@ -5122,7 +5386,7 @@ namespace DxMessaging.Core.MessageBus
5122
5386
  return true;
5123
5387
  }
5124
5388
 
5125
- private Action InternalRegisterUntargeted<T>(
5389
+ private MessageBusRegistration InternalRegisterUntargeted<T>(
5126
5390
  MessageHandler messageHandler,
5127
5391
  MessageCache<HandlerCache<int, HandlerCache>> sinks,
5128
5392
  RegistrationMethod registrationMethod,
@@ -5172,7 +5436,7 @@ namespace DxMessaging.Core.MessageBus
5172
5436
  }
5173
5437
  StageDispatchSnapshot<T>(this, capturedHandlers, slotKey);
5174
5438
  Type type = typeof(T);
5175
- _log.Log(
5439
+ _log?.Log(
5176
5440
  new MessagingRegistration(
5177
5441
  handlerOwnerId,
5178
5442
  type,
@@ -5182,49 +5446,66 @@ namespace DxMessaging.Core.MessageBus
5182
5446
  );
5183
5447
 
5184
5448
  long capturedGeneration = _resetGeneration;
5185
- return () =>
5186
- {
5187
- // Generation guard: if ResetState() ran after this closure was
5188
- // captured (e.g. a deferred Object.Destroy fires after a
5189
- // domain-reload-style reset), silently no-op rather than
5190
- // logging a misleading over-deregistration error.
5191
- if (capturedGeneration != _resetGeneration)
5192
- {
5193
- return;
5194
- }
5449
+ return new MessageBusRegistration(
5450
+ MessageBusRegistration.Kind.Handler,
5451
+ registrationMethod,
5452
+ priority,
5453
+ capturedGeneration,
5454
+ 0L,
5455
+ touchTick,
5456
+ capturedHandlers,
5457
+ null,
5458
+ messageHandler,
5459
+ default
5460
+ );
5461
+ }
5195
5462
 
5196
- long deregisterTouchTick = AdvanceTick();
5197
- InvalidateDispatchPlans();
5198
- cache.version++;
5199
- if (
5200
- !sinks.TryGetValue<T>(out handlers)
5201
- || !ReferenceEquals(handlers, capturedHandlers)
5202
- || !handlers.handlers.TryGetValue(priority, out cache)
5203
- || !cache.handlers.TryGetValue(messageHandler, out count)
5204
- )
5205
- {
5206
- if (
5207
- capturedHandlers.handlers.Count == 0
5208
- && !ReferenceEquals(handlers, capturedHandlers)
5209
- )
5210
- {
5211
- return;
5212
- }
5463
+ /// <summary>
5464
+ /// Re-expression of the former <see cref="InternalRegisterUntargeted{T}"/> closure body:
5465
+ /// the scalar (untargeted / without-context) handler deregistration. Behaviour and the
5466
+ /// four reentrancy invariants are unchanged; the closure's captured locals are read from
5467
+ /// <paramref name="reg"/>'s fields instead, and the sink is re-resolved from the method.
5468
+ /// </summary>
5469
+ private void DeregisterScalarHandler<T>(in MessageBusRegistration reg)
5470
+ where T : IMessage
5471
+ {
5472
+ // Generation guard: if ResetState() ran after this handle was captured (e.g. a
5473
+ // deferred Object.Destroy fires after a domain-reload-style reset), silently no-op
5474
+ // rather than logging a misleading over-deregistration error.
5475
+ if (reg.generation != _resetGeneration)
5476
+ {
5477
+ return;
5478
+ }
5213
5479
 
5214
- if (MessagingDebug.enabled)
5215
- {
5216
- MessagingDebug.Log(
5217
- LogLevel.Error,
5218
- "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5219
- type,
5220
- messageHandler
5221
- );
5222
- }
5480
+ MessageHandler messageHandler = (MessageHandler)reg.payload;
5481
+ HandlerCache<int, HandlerCache> capturedHandlers =
5482
+ (HandlerCache<int, HandlerCache>)reg.capturedPrimary;
5483
+ int priority = reg.priority;
5484
+ RegistrationMethod registrationMethod = reg.method;
5485
+ SlotKey slotKey = RegistrationMethodAxes.GetSlotKey(registrationMethod);
5486
+ Type type = typeof(T);
5487
+ InstanceId handlerOwnerId = messageHandler.owner;
5223
5488
 
5224
- return;
5225
- }
5489
+ long deregisterTouchTick = AdvanceTick();
5490
+ InvalidateDispatchPlans();
5226
5491
 
5227
- _log.Log(
5492
+ // FAST PATH: operate on the captured leaf handler-cache (already pinned on the
5493
+ // handle at registration time) DIRECTLY, without re-resolving the sink from the
5494
+ // method (ScalarSinkForMethod) or re-walking sinks->type->priority->handler. When
5495
+ // the generation guard above has passed and the captured bucket still holds this
5496
+ // handler, the captured leaf IS the live sink entry (handles are unique and never
5497
+ // reused), so the re-resolution + ReferenceEquals identity check is redundant. The
5498
+ // sweep-staleness / over-deregistration classification only matters when the handler
5499
+ // is NOT found, so it is deferred to the cold fallback below. This removes the
5500
+ // per-deregistration sink re-resolution (the measured cold-path regression source)
5501
+ // while preserving every guard and the throw-safe ordering (this method performs no
5502
+ // user callback; the throwing IMessageBus.Deregister boundary is the caller's).
5503
+ if (
5504
+ capturedHandlers.handlers.TryGetValue(priority, out HandlerCache cache)
5505
+ && cache.handlers.TryGetValue(messageHandler, out int count)
5506
+ )
5507
+ {
5508
+ _log?.Log(
5228
5509
  new MessagingRegistration(
5229
5510
  handlerOwnerId,
5230
5511
  type,
@@ -5232,30 +5513,27 @@ namespace DxMessaging.Core.MessageBus
5232
5513
  registrationMethod
5233
5514
  )
5234
5515
  );
5235
- Touch(handlers, deregisterTouchTick);
5236
- handlers.version++;
5237
- handler = cache.handlers;
5516
+ Touch(capturedHandlers, deregisterTouchTick);
5517
+ capturedHandlers.version++;
5518
+ cache.version++;
5519
+ Dictionary<MessageHandler, int> handler = cache.handlers;
5238
5520
  if (count <= 1)
5239
5521
  {
5240
- bool complete = handler.Remove(messageHandler);
5241
- // List.Remove is O(n) over the same-priority bucket.
5242
- // Accepted tradeoff (here and at the context-path sibling
5243
- // site): buckets are small in practice, removal is a cold
5244
- // churn path, and the list keeps dispatch-order rebuilds
5245
- // allocation-free while preserving first-registration
5246
- // order, unlike Dictionary enumeration whose freed slots
5247
- // are reused LIFO. Mirrors the MessageHandler-side
5522
+ _ = handler.Remove(messageHandler);
5523
+ // List.Remove is O(n) over the same-priority bucket. Accepted tradeoff (here
5524
+ // and at the context-path sibling site): buckets are small in practice,
5525
+ // removal is a cold churn path, and the list keeps dispatch-order rebuilds
5526
+ // allocation-free while preserving first-registration order, unlike Dictionary
5527
+ // enumeration whose freed slots are reused LIFO. Mirrors the MessageHandler-side
5248
5528
  // insertionOrder tradeoff.
5249
5529
  _ = cache.insertionOrder.Remove(messageHandler);
5250
5530
  MarkDirtyHandler(messageHandler);
5251
- cache.version++;
5252
- // do not mutate cache.cache here; let next read rebuild from handlers
5253
5531
 
5254
5532
  if (handler.Count == 0)
5255
5533
  {
5256
- _ = handlers.handlers.Remove(priority);
5534
+ _ = capturedHandlers.handlers.Remove(priority);
5257
5535
  // remove priority from order
5258
- List<int> order = handlers.order;
5536
+ List<int> order = capturedHandlers.order;
5259
5537
  int removeIdx = order.IndexOf(priority);
5260
5538
  if (removeIdx >= 0)
5261
5539
  {
@@ -5263,30 +5541,51 @@ namespace DxMessaging.Core.MessageBus
5263
5541
  }
5264
5542
  }
5265
5543
 
5266
- if (handlers.handlers.Count == 0)
5544
+ if (capturedHandlers.handlers.Count == 0)
5267
5545
  {
5268
5546
  MarkDirtyType<T>();
5269
5547
  }
5270
-
5271
- if (!complete && MessagingDebug.enabled)
5272
- {
5273
- MessagingDebug.Log(
5274
- LogLevel.Error,
5275
- "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5276
- type,
5277
- messageHandler
5278
- );
5279
- }
5280
5548
  }
5281
5549
  else
5282
5550
  {
5283
5551
  handler[messageHandler] = count - 1;
5284
5552
  }
5285
- StageDispatchSnapshot<T>(this, handlers, slotKey);
5286
- };
5553
+ StageDispatchSnapshot<T>(this, capturedHandlers, slotKey);
5554
+ return;
5555
+ }
5556
+
5557
+ // COLD FALLBACK: the handler was not found in the captured bucket, so this is a no-op
5558
+ // deregistration (post-sweep, or a genuine over-deregistration) -- nothing is removed,
5559
+ // so NO version bump / snapshot invalidation is performed. Bumping here would
5560
+ // spuriously rebuild whichever bucket currently occupies this priority, which after a
5561
+ // full deregister + re-registration at the same priority is the NEW live bucket (the
5562
+ // stale handle must not touch it). Re-resolve the live sink only to CLASSIFY this as a
5563
+ // silent stale-after-sweep no-op versus a genuine over-deregistration. Rare path; not
5564
+ // on the steady deregistration cost.
5565
+ MessageCache<HandlerCache<int, HandlerCache>> sinks = ScalarSinkForMethod(
5566
+ registrationMethod
5567
+ );
5568
+ _ = sinks.TryGetValue<T>(out HandlerCache<int, HandlerCache> handlers);
5569
+ if (
5570
+ capturedHandlers.handlers.Count == 0
5571
+ && !ReferenceEquals(handlers, capturedHandlers)
5572
+ )
5573
+ {
5574
+ return;
5575
+ }
5576
+
5577
+ if (MessagingDebug.enabled)
5578
+ {
5579
+ MessagingDebug.Log(
5580
+ LogLevel.Error,
5581
+ "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5582
+ type,
5583
+ messageHandler
5584
+ );
5585
+ }
5287
5586
  }
5288
5587
 
5289
- private Action InternalRegisterWithContext<T>(
5588
+ private MessageBusRegistration InternalRegisterWithContext<T>(
5290
5589
  InstanceId context,
5291
5590
  MessageHandler messageHandler,
5292
5591
  MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks,
@@ -5351,7 +5650,7 @@ namespace DxMessaging.Core.MessageBus
5351
5650
  }
5352
5651
 
5353
5652
  Type type = typeof(T);
5354
- _log.Log(
5653
+ _log?.Log(
5355
5654
  new MessagingRegistration(
5356
5655
  context,
5357
5656
  type,
@@ -5362,46 +5661,60 @@ namespace DxMessaging.Core.MessageBus
5362
5661
  StageDispatchSnapshot<T>(this, handlers, slotKey);
5363
5662
 
5364
5663
  long capturedGeneration = _resetGeneration;
5365
- return () =>
5366
- {
5367
- // Generation guard: see InternalRegisterUntargeted for the
5368
- // rationale. Skip silently when the closure outlived a Reset.
5369
- if (capturedGeneration != _resetGeneration)
5370
- {
5371
- return;
5372
- }
5664
+ return new MessageBusRegistration(
5665
+ MessageBusRegistration.Kind.Handler,
5666
+ registrationMethod,
5667
+ priority,
5668
+ capturedGeneration,
5669
+ 0L,
5670
+ touchTick,
5671
+ capturedHandlers,
5672
+ capturedBroadcastHandlers,
5673
+ messageHandler,
5674
+ context
5675
+ );
5676
+ }
5373
5677
 
5374
- long deregisterTouchTick = AdvanceTick();
5375
- InvalidateDispatchPlans();
5376
- cache.version++;
5377
- if (
5378
- !sinks.TryGetValue<T>(out broadcastHandlers)
5379
- || !ReferenceEquals(broadcastHandlers, capturedBroadcastHandlers)
5380
- || !broadcastHandlers.TryGetValue(context, out handlers)
5381
- || !ReferenceEquals(handlers, capturedHandlers)
5382
- || !handlers.handlers.TryGetValue(priority, out cache)
5383
- || !cache.handlers.TryGetValue(messageHandler, out count)
5384
- )
5385
- {
5386
- if (IsStaleContextDeregisterAfterSweep<T>(sinks, context, capturedHandlers))
5387
- {
5388
- return;
5389
- }
5678
+ /// <summary>
5679
+ /// Re-expression of the former <see cref="InternalRegisterWithContext{T}"/> closure body:
5680
+ /// the keyed (targeted / sourced-broadcast) handler deregistration. Behaviour and the four
5681
+ /// reentrancy invariants are unchanged; the closure's captured locals are read from
5682
+ /// <paramref name="reg"/>'s fields, and the sink is re-resolved from the method.
5683
+ /// </summary>
5684
+ private void DeregisterContextHandler<T>(in MessageBusRegistration reg)
5685
+ where T : IMessage
5686
+ {
5687
+ // Generation guard: see DeregisterScalarHandler for the rationale. Skip silently when
5688
+ // the handle outlived a Reset.
5689
+ if (reg.generation != _resetGeneration)
5690
+ {
5691
+ return;
5692
+ }
5390
5693
 
5391
- if (MessagingDebug.enabled)
5392
- {
5393
- MessagingDebug.Log(
5394
- LogLevel.Error,
5395
- "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5396
- type,
5397
- messageHandler
5398
- );
5399
- }
5694
+ MessageHandler messageHandler = (MessageHandler)reg.payload;
5695
+ HandlerCache<int, HandlerCache> capturedHandlers =
5696
+ (HandlerCache<int, HandlerCache>)reg.capturedPrimary;
5697
+ InstanceId context = reg.context;
5698
+ int priority = reg.priority;
5699
+ RegistrationMethod registrationMethod = reg.method;
5700
+ SlotKey slotKey = RegistrationMethodAxes.GetSlotKey(registrationMethod);
5701
+ Type type = typeof(T);
5400
5702
 
5401
- return;
5402
- }
5703
+ long deregisterTouchTick = AdvanceTick();
5704
+ InvalidateDispatchPlans();
5403
5705
 
5404
- _log.Log(
5706
+ // FAST PATH: operate on the captured per-context leaf handler-cache directly, without
5707
+ // re-resolving the sink (ContextSinkForMethod) or re-walking sinks->type->context->
5708
+ // priority->handler. See DeregisterScalarHandler for the full rationale and the
5709
+ // sweep-staleness argument (the context sweep, like the scalar sweep, only evicts
5710
+ // EMPTY caches, so a found handler is never in a swept/detached cache). Throw-safe:
5711
+ // no user callback runs here.
5712
+ if (
5713
+ capturedHandlers.handlers.TryGetValue(priority, out HandlerCache cache)
5714
+ && cache.handlers.TryGetValue(messageHandler, out int count)
5715
+ )
5716
+ {
5717
+ _log?.Log(
5405
5718
  new MessagingRegistration(
5406
5719
  context,
5407
5720
  type,
@@ -5409,23 +5722,22 @@ namespace DxMessaging.Core.MessageBus
5409
5722
  registrationMethod
5410
5723
  )
5411
5724
  );
5412
- Touch(handlers, deregisterTouchTick);
5413
- handler = cache.handlers;
5725
+ Touch(capturedHandlers, deregisterTouchTick);
5726
+ cache.version++;
5727
+ Dictionary<MessageHandler, int> handler = cache.handlers;
5414
5728
  if (count <= 1)
5415
5729
  {
5416
- bool complete = handler.Remove(messageHandler);
5417
- // O(n) List.Remove: see the tradeoff comment at the
5418
- // scalar-path sibling site in InternalRegisterUntargeted.
5730
+ _ = handler.Remove(messageHandler);
5731
+ // O(n) List.Remove: see the tradeoff comment at the scalar-path sibling site in
5732
+ // DeregisterScalarHandler.
5419
5733
  _ = cache.insertionOrder.Remove(messageHandler);
5420
5734
  MarkDirtyHandler(messageHandler);
5421
- cache.version++;
5422
- // do not mutate cache.cache here; let next read rebuild from handlers
5423
5735
  if (handler.Count == 0)
5424
5736
  {
5425
- handlers.version++;
5426
- _ = handlers.handlers.Remove(priority);
5737
+ capturedHandlers.version++;
5738
+ _ = capturedHandlers.handlers.Remove(priority);
5427
5739
  // remove priority from order
5428
- List<int> order = handlers.order;
5740
+ List<int> order = capturedHandlers.order;
5429
5741
  int removeIdx = order.IndexOf(priority);
5430
5742
  if (removeIdx >= 0)
5431
5743
  {
@@ -5433,29 +5745,265 @@ namespace DxMessaging.Core.MessageBus
5433
5745
  }
5434
5746
  }
5435
5747
 
5436
- if (handlers.handlers.Count == 0)
5748
+ if (capturedHandlers.handlers.Count == 0)
5437
5749
  {
5438
5750
  MarkDirtyTarget<T>(context);
5439
5751
  }
5440
-
5441
- if (!complete && MessagingDebug.enabled)
5442
- {
5443
- MessagingDebug.Log(
5444
- LogLevel.Error,
5445
- "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5446
- type,
5447
- messageHandler
5448
- );
5449
- }
5450
5752
  }
5451
5753
  else
5452
5754
  {
5453
5755
  handler[messageHandler] = count - 1;
5454
5756
  }
5455
- StageDispatchSnapshot<T>(this, handlers, slotKey);
5757
+ StageDispatchSnapshot<T>(this, capturedHandlers, slotKey);
5758
+ return;
5759
+ }
5760
+
5761
+ // COLD FALLBACK: handler not found in the captured bucket -> a no-op deregistration
5762
+ // (nothing removed), so NO version bump is performed (it would spuriously rebuild the
5763
+ // bucket currently at this priority -- the NEW live bucket after a full deregister +
5764
+ // re-registration). Re-resolve only to CLASSIFY stale-after-sweep (silent) versus
5765
+ // over-deregistration (error). Rare path; not on the steady deregistration cost.
5766
+ MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks =
5767
+ ContextSinkForMethod(registrationMethod);
5768
+ if (IsStaleContextDeregisterAfterSweep<T>(sinks, context, capturedHandlers))
5769
+ {
5770
+ return;
5771
+ }
5772
+
5773
+ if (MessagingDebug.enabled)
5774
+ {
5775
+ MessagingDebug.Log(
5776
+ LogLevel.Error,
5777
+ "Received over-deregistration of {0} for {1}. Check to make sure you're not calling (de)registration multiple times.",
5778
+ type,
5779
+ messageHandler
5780
+ );
5781
+ }
5782
+ }
5783
+
5784
+ /// <inheritdoc />
5785
+ public void Deregister<T>(in MessageBusRegistration registration)
5786
+ where T : IMessage
5787
+ {
5788
+ switch (registration.kind)
5789
+ {
5790
+ case MessageBusRegistration.Kind.Handler:
5791
+ if (IsContextMethod(registration.method))
5792
+ {
5793
+ DeregisterContextHandler<T>(in registration);
5794
+ }
5795
+ else
5796
+ {
5797
+ DeregisterScalarHandler<T>(in registration);
5798
+ }
5799
+ break;
5800
+ case MessageBusRegistration.Kind.UntargetedInterceptor:
5801
+ case MessageBusRegistration.Kind.TargetedInterceptor:
5802
+ case MessageBusRegistration.Kind.BroadcastInterceptor:
5803
+ DeregisterInterceptor<T>(in registration);
5804
+ break;
5805
+ case MessageBusRegistration.Kind.GlobalAcceptAll:
5806
+ DeregisterGlobalAcceptAll(in registration);
5807
+ break;
5808
+ default:
5809
+ // Kind.None is the empty/sentinel handle (no-op); Kind.External handles are
5810
+ // minted by a foreign IMessageBus implementation and own no store here.
5811
+ break;
5812
+ }
5813
+ }
5814
+
5815
+ /// <summary>
5816
+ /// Re-expression of the former interceptor closure body. The interceptor store is selected
5817
+ /// by <see cref="MessageBusRegistration.kind"/> (all three interceptor registrars log the
5818
+ /// single <see cref="RegistrationMethod.Interceptor"/>, so the method cannot discriminate).
5819
+ /// </summary>
5820
+ private void DeregisterInterceptor<T>(in MessageBusRegistration reg)
5821
+ where T : IMessage
5822
+ {
5823
+ // Generation guard: see DeregisterScalarHandler.
5824
+ if (reg.generation != _resetGeneration)
5825
+ {
5826
+ return;
5827
+ }
5828
+
5829
+ MessageCache<InterceptorCache<object>> interceptsByType = reg.kind switch
5830
+ {
5831
+ MessageBusRegistration.Kind.UntargetedInterceptor => _untargetedInterceptsByType,
5832
+ MessageBusRegistration.Kind.TargetedInterceptor => _targetedInterceptsByType,
5833
+ MessageBusRegistration.Kind.BroadcastInterceptor => _broadcastInterceptsByType,
5834
+ _ => null,
5456
5835
  };
5836
+ if (interceptsByType == null)
5837
+ {
5838
+ return;
5839
+ }
5840
+
5841
+ InterceptorCache<object> capturedInterceptors =
5842
+ (InterceptorCache<object>)reg.capturedPrimary;
5843
+ object interceptor = reg.payload;
5844
+ int priority = reg.priority;
5845
+
5846
+ if (IsStaleInterceptorDeregisterAfterSweep<T>(interceptsByType, capturedInterceptors))
5847
+ {
5848
+ return;
5849
+ }
5850
+
5851
+ _ = AdvanceTick();
5852
+ InvalidateDispatchPlans();
5853
+ capturedInterceptors.lastTouchTicks = _tickCounter;
5854
+ MarkDirtyType<T>();
5855
+ _log?.Log(
5856
+ new MessagingRegistration(
5857
+ InstanceId.EmptyId,
5858
+ typeof(T),
5859
+ RegistrationType.Deregister,
5860
+ RegistrationMethod.Interceptor
5861
+ )
5862
+ );
5863
+ bool removed = false;
5864
+ if (
5865
+ _uniqueInterceptorsAndPriorities.TryGetValue(
5866
+ interceptor,
5867
+ out Dictionary<int, int> priorityCount
5868
+ )
5869
+ )
5870
+ {
5871
+ if (priorityCount.TryGetValue(priority, out int count))
5872
+ {
5873
+ if (1 < count)
5874
+ {
5875
+ priorityCount[priority] = count - 1;
5876
+ }
5877
+ else
5878
+ {
5879
+ removed = true;
5880
+ _ = priorityCount.Remove(priority);
5881
+ }
5882
+ }
5883
+
5884
+ if (priorityCount.Count == 0)
5885
+ {
5886
+ _uniqueInterceptorsAndPriorities.Remove(interceptor);
5887
+ }
5888
+ }
5889
+ else if (MessagingDebug.enabled)
5890
+ {
5891
+ MessagingDebug.Log(
5892
+ LogLevel.Error,
5893
+ "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
5894
+ interceptor
5895
+ );
5896
+ }
5897
+
5898
+ bool complete = false;
5899
+ if (removed)
5900
+ {
5901
+ if (
5902
+ interceptsByType.TryGetValue<T>(
5903
+ out InterceptorCache<object> prioritizedInterceptors
5904
+ )
5905
+ )
5906
+ {
5907
+ if (
5908
+ prioritizedInterceptors.handlers.TryGetValue(
5909
+ priority,
5910
+ out List<object> interceptors
5911
+ )
5912
+ )
5913
+ {
5914
+ complete = interceptors.Remove(interceptor);
5915
+ if (interceptors.Count == 0)
5916
+ {
5917
+ _ = prioritizedInterceptors.handlers.Remove(priority);
5918
+ }
5919
+ }
5920
+ }
5921
+
5922
+ if (!complete && MessagingDebug.enabled)
5923
+ {
5924
+ MessagingDebug.Log(
5925
+ LogLevel.Error,
5926
+ "Received over-deregistration of Interceptor {0}. Check to make sure you're not calling (de)registration multiple times.",
5927
+ interceptor
5928
+ );
5929
+ }
5930
+ }
5457
5931
  }
5458
5932
 
5933
+ /// <summary>
5934
+ /// True for the keyed (targeted / sourced-broadcast) handler registration methods, whose
5935
+ /// deregistration runs <see cref="DeregisterContextHandler{T}"/>; false for the scalar
5936
+ /// (untargeted / without-context) methods. Mirrors the register-side sink routing.
5937
+ /// </summary>
5938
+ private static bool IsContextMethod(RegistrationMethod method) =>
5939
+ method switch
5940
+ {
5941
+ RegistrationMethod.Targeted
5942
+ or RegistrationMethod.Broadcast
5943
+ or RegistrationMethod.TargetedPostProcessor
5944
+ or RegistrationMethod.BroadcastPostProcessor => true,
5945
+ _ => false,
5946
+ };
5947
+
5948
+ /// <summary>
5949
+ /// Reverse of the register-side hardcoded scalar-sink selection (e.g.
5950
+ /// <c>_scalarSinks[BusSinkIndex.UntargetedHandleDefault]</c>): maps a scalar handler method
5951
+ /// back to its sink so <see cref="DeregisterScalarHandler{T}"/> can re-resolve it.
5952
+ /// </summary>
5953
+ private MessageCache<HandlerCache<int, HandlerCache>> ScalarSinkForMethod(
5954
+ RegistrationMethod method
5955
+ ) =>
5956
+ method switch
5957
+ {
5958
+ RegistrationMethod.Untargeted => _scalarSinks[BusSinkIndex.UntargetedHandleDefault],
5959
+ RegistrationMethod.BroadcastWithoutSource => _scalarSinks[
5960
+ BusSinkIndex.BroadcastHandleWithoutContext
5961
+ ],
5962
+ RegistrationMethod.TargetedWithoutTargeting => _scalarSinks[
5963
+ BusSinkIndex.TargetedHandleWithoutContext
5964
+ ],
5965
+ RegistrationMethod.UntargetedPostProcessor => _scalarSinks[
5966
+ BusSinkIndex.UntargetedPostProcessDefault
5967
+ ],
5968
+ RegistrationMethod.TargetedWithoutTargetingPostProcessor => _scalarSinks[
5969
+ BusSinkIndex.TargetedPostProcessWithoutContext
5970
+ ],
5971
+ RegistrationMethod.BroadcastWithoutSourcePostProcessor => _scalarSinks[
5972
+ BusSinkIndex.BroadcastPostProcessWithoutContext
5973
+ ],
5974
+ _ => throw new ArgumentOutOfRangeException(
5975
+ nameof(method),
5976
+ method,
5977
+ "Not a scalar handler registration method."
5978
+ ),
5979
+ };
5980
+
5981
+ /// <summary>
5982
+ /// Reverse of the register-side hardcoded context-sink selection: maps a keyed handler
5983
+ /// method back to its sink so <see cref="DeregisterContextHandler{T}"/> can re-resolve it.
5984
+ /// </summary>
5985
+ private MessageCache<
5986
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>>
5987
+ > ContextSinkForMethod(RegistrationMethod method) =>
5988
+ method switch
5989
+ {
5990
+ RegistrationMethod.Targeted => _contextSinks[BusContextIndex.TargetedHandleDefault],
5991
+ RegistrationMethod.Broadcast => _contextSinks[
5992
+ BusContextIndex.BroadcastHandleDefault
5993
+ ],
5994
+ RegistrationMethod.TargetedPostProcessor => _contextSinks[
5995
+ BusContextIndex.TargetedPostProcessDefault
5996
+ ],
5997
+ RegistrationMethod.BroadcastPostProcessor => _contextSinks[
5998
+ BusContextIndex.BroadcastPostProcessDefault
5999
+ ],
6000
+ _ => throw new ArgumentOutOfRangeException(
6001
+ nameof(method),
6002
+ method,
6003
+ "Not a context handler registration method."
6004
+ ),
6005
+ };
6006
+
5459
6007
  private static bool IsStaleContextDeregisterAfterSweep<T>(
5460
6008
  MessageCache<Dictionary<InstanceId, HandlerCache<int, HandlerCache>>> sinks,
5461
6009
  InstanceId context,
@@ -5587,7 +6135,7 @@ namespace DxMessaging.Core.MessageBus
5587
6135
  {
5588
6136
  DebugAssertAcquireFastPrecondition(handlers);
5589
6137
  DispatchState state = handlers.dispatchState;
5590
- if (state != null && !state.hasPending && !state.active.IsEmpty)
6138
+ if (state != null && !state.hasPending && state.active.IsInitialized)
5591
6139
  {
5592
6140
  handlers.lastTouchTicks = messageBus._tickCounter;
5593
6141
  state.snapshotEmissionId = emissionId;
@@ -5645,7 +6193,7 @@ namespace DxMessaging.Core.MessageBus
5645
6193
 
5646
6194
  if (state.hasPending)
5647
6195
  {
5648
- if (state.pendingDirty || (hasHandlers && state.pending.IsEmpty))
6196
+ if (state.pendingDirty || (hasHandlers && !state.pending.IsInitialized))
5649
6197
  {
5650
6198
  ReleaseSnapshot(ref state.pending);
5651
6199
  state.pending = hasHandlers
@@ -5655,7 +6203,7 @@ namespace DxMessaging.Core.MessageBus
5655
6203
  state.pendingDirty = false;
5656
6204
  }
5657
6205
  }
5658
- else if (state.active.IsEmpty && hasHandlers)
6206
+ else if (!state.active.IsInitialized && hasHandlers)
5659
6207
  {
5660
6208
  ReleaseSnapshot(ref state.pending);
5661
6209
  state.pending = BuildDispatchSnapshot<TMessage>(
@@ -5678,7 +6226,7 @@ namespace DxMessaging.Core.MessageBus
5678
6226
  // ReleaseDisplacedSnapshot defers the release to the
5679
6227
  // outermost dispatch-lease exit when one is in flight.
5680
6228
  messageBus.ReleaseDisplacedSnapshot(ref state.active);
5681
- if (state.pendingDirty || (hasHandlers && state.pending.IsEmpty))
6229
+ if (state.pendingDirty || (hasHandlers && !state.pending.IsInitialized))
5682
6230
  {
5683
6231
  ReleaseSnapshot(ref state.pending);
5684
6232
  state.pending = hasHandlers
@@ -5698,7 +6246,7 @@ namespace DxMessaging.Core.MessageBus
5698
6246
  state.hasPending = false;
5699
6247
  state.pendingDirty = false;
5700
6248
  }
5701
- else if (!hasHandlers && !state.active.IsEmpty)
6249
+ else if (!hasHandlers && state.active.IsInitialized)
5702
6250
  {
5703
6251
  messageBus.ReleaseDisplacedSnapshot(ref state.active);
5704
6252
  }
@@ -5722,46 +6270,13 @@ namespace DxMessaging.Core.MessageBus
5722
6270
  return DispatchSnapshot.Empty;
5723
6271
  }
5724
6272
 
5725
- List<int> orderedPriorities = handlers.order;
5726
- int priorityCount = orderedPriorities.Count;
5727
- DispatchBucket[] buckets = DispatchBucketPool.Rent(priorityCount);
5728
-
5729
- for (int i = 0; i < priorityCount; ++i)
5730
- {
5731
- int priority = orderedPriorities[i];
5732
- if (
5733
- !handlers.handlers.TryGetValue(priority, out HandlerCache cache)
5734
- || cache == null
5735
- )
5736
- {
5737
- buckets[i] = DispatchBucket.CreateEmpty(priority);
5738
- continue;
5739
- }
5740
-
5741
- Dictionary<MessageHandler, int> handlerLookup = cache.handlers;
5742
- if (handlerLookup == null || handlerLookup.Count == 0)
5743
- {
5744
- buckets[i] = DispatchBucket.CreateEmpty(priority);
5745
- continue;
5746
- }
5747
-
5748
- // Count-only bucket: non-global dispatch walks the resolved
5749
- // flat array exclusively, so the bucket carries the handler
5750
- // count (consumed by IsEmpty / HasAnyDispatchEntries for the
5751
- // legacy "found any handlers" reporting) without renting or
5752
- // filling a DispatchEntry array.
5753
- DebugAssertBusInsertionOrderInSync(cache);
5754
- buckets[i] = new DispatchBucket(
5755
- priority,
5756
- Array.Empty<DispatchEntry>(),
5757
- handlerLookup.Count,
5758
- pooledEntries: false
5759
- );
5760
- }
5761
-
5762
- DispatchSnapshot snapshot = new DispatchSnapshot(buckets, priorityCount, pooled: true);
5763
- snapshot.flat = BuildFlatDispatch<TMessage>(messageBus, handlers, slotKey, context);
5764
- return snapshot;
6273
+ FlatDispatchArray flat = BuildFlatDispatch<TMessage>(
6274
+ messageBus,
6275
+ handlers,
6276
+ slotKey,
6277
+ context
6278
+ );
6279
+ return new DispatchSnapshot(flat, hasRegistrations: true);
5765
6280
  }
5766
6281
 
5767
6282
  /// <summary>
@@ -6288,24 +6803,12 @@ namespace DxMessaging.Core.MessageBus
6288
6803
  );
6289
6804
  }
6290
6805
 
6291
- // IL2CPP check elision: buckets[0..bucketCount) is non-null by snapshot
6292
- // construction; same justification as DispatchFlatSnapshot.
6293
6806
  [Il2CppSetOption(Option.NullChecks, false)]
6294
6807
  [Il2CppSetOption(Option.ArrayBoundsChecks, false)]
6295
6808
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
6296
6809
  private static bool HasAnyDispatchEntries(DispatchSnapshot snapshot)
6297
6810
  {
6298
- DispatchBucket[] buckets = snapshot.buckets;
6299
- int bucketCount = snapshot.bucketCount;
6300
- for (int i = 0; i < bucketCount; ++i)
6301
- {
6302
- if (0 < buckets[i].entryCount)
6303
- {
6304
- return true;
6305
- }
6306
- }
6307
-
6308
- return false;
6811
+ return snapshot.hasRegistrations;
6309
6812
  }
6310
6813
 
6311
6814
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -6330,7 +6833,7 @@ namespace DxMessaging.Core.MessageBus
6330
6833
 
6331
6834
  if (state.hasPending)
6332
6835
  {
6333
- if (state.pendingDirty || (hasHandlers && state.pending.IsEmpty))
6836
+ if (state.pendingDirty || (hasHandlers && !state.pending.IsInitialized))
6334
6837
  {
6335
6838
  ReleaseSnapshot(ref state.pending);
6336
6839
  if (hasHandlers)
@@ -6349,7 +6852,7 @@ namespace DxMessaging.Core.MessageBus
6349
6852
  state.pendingDirty = false;
6350
6853
  }
6351
6854
  }
6352
- else if (state.active.IsEmpty && hasHandlers)
6855
+ else if (!state.active.IsInitialized && hasHandlers)
6353
6856
  {
6354
6857
  ReleaseSnapshot(ref state.pending);
6355
6858
  state.pending = BuildGlobalDispatchSnapshot<TMessage>(messageBus, handlers, kind);
@@ -6365,7 +6868,7 @@ namespace DxMessaging.Core.MessageBus
6365
6868
  // snapshot may still be iterated by an outer emission, so
6366
6869
  // its release is deferred while a dispatch lease is live.
6367
6870
  messageBus.ReleaseDisplacedSnapshot(ref state.active);
6368
- if (state.pendingDirty || (hasHandlers && state.pending.IsEmpty))
6871
+ if (state.pendingDirty || (hasHandlers && !state.pending.IsInitialized))
6369
6872
  {
6370
6873
  ReleaseSnapshot(ref state.pending);
6371
6874
  state.pending = hasHandlers
@@ -6380,7 +6883,7 @@ namespace DxMessaging.Core.MessageBus
6380
6883
  state.hasPending = false;
6381
6884
  state.pendingDirty = false;
6382
6885
  }
6383
- else if (!hasHandlers && !state.active.IsEmpty)
6886
+ else if (!hasHandlers && state.active.IsInitialized)
6384
6887
  {
6385
6888
  messageBus.ReleaseDisplacedSnapshot(ref state.active);
6386
6889
  }
@@ -6414,7 +6917,13 @@ namespace DxMessaging.Core.MessageBus
6414
6917
  }
6415
6918
 
6416
6919
  buckets[0] = new DispatchBucket(0, entries, entryCount, pooledEntries: true);
6417
- return new DispatchSnapshot(buckets, 1, pooled: true);
6920
+ return new DispatchSnapshot(
6921
+ buckets,
6922
+ 1,
6923
+ entryCount,
6924
+ hasRegistrations: true,
6925
+ pooled: true
6926
+ );
6418
6927
  }
6419
6928
 
6420
6929
  [MethodImpl(MethodImplOptions.AggressiveInlining)]