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
@@ -295,477 +295,356 @@ namespace DxMessaging.Core
295
295
  }
296
296
 
297
297
  /// <summary>
298
- /// Callback from the MessageBus for handling UntargetedMessages - user code should generally never use this.
298
+ /// Callback from the MessageBus for handling Messages when this MessageHandler has subscribed to GlobalAcceptAll - user code should generally never use this.
299
299
  /// </summary>
300
- /// <note>
301
- /// In this case, "UntargetedMessage" refers to Targeted without targeting, and UntargetedMessages, hence T : IMessage.
302
- /// </note>
303
300
  /// <param name="message">Message to handle.</param>
304
301
  /// <param name="messageBus">The specific MessageBus to use.</param>
305
- /// <param name="priority">Priority at which to run the handlers.</param>
306
- public void HandleUntargetedMessage<TMessage>(
307
- ref TMessage message,
308
- IMessageBus messageBus,
309
- int priority
302
+ internal void HandleGlobalUntargetedMessage(
303
+ ref IUntargetedMessage message,
304
+ IMessageBus messageBus
310
305
  )
311
- where TMessage : IMessage
312
306
  {
313
307
  if (!active)
314
308
  {
315
309
  return;
316
310
  }
317
311
 
318
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
312
+ // Use the "IMessage" explicitly to indicate global messages, allowing us to multipurpose a single dictionary
313
+ if (GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
319
314
  {
320
315
  long emissionId = messageBus.EmissionId;
321
- handler.HandleUntargeted(ref message, priority, emissionId);
316
+ handler.HandleGlobalUntargeted(ref message, emissionId);
322
317
  }
323
318
  }
324
319
 
325
320
  /// <summary>
326
- /// Callback from the MessageBus for handling UntargetedMessages - user code should generally never use this.
321
+ /// Pre-freezes this handler's GlobalAcceptAll untargeted caches for this emission.
327
322
  /// </summary>
328
- /// <note>
329
- /// In this case, "UntargetedMessage" refers to Targeted without targeting, and UntargetedMessages, hence T : IUntargetedMessage.
330
- /// </note>
331
- /// <param name="message">Message to handle.</param>
332
- /// <param name="messageBus">The specific MessageBus to use.</param>
333
- /// <param name="priority">Priority at which to run the handlers.</param>
334
- public void HandleUntargetedPostProcessing<TMessage>(
335
- ref TMessage message,
336
- IMessageBus messageBus,
337
- int priority
338
- )
339
- where TMessage : IUntargetedMessage
323
+ internal void PrefreezeGlobalUntargetedForEmission(long emissionId, IMessageBus messageBus)
340
324
  {
341
- if (!active)
325
+ if (!GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
342
326
  {
343
327
  return;
344
328
  }
345
329
 
346
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
347
- {
348
- long emissionId = messageBus.EmissionId;
349
- handler.HandleUntargetedPostProcessing(ref message, priority, emissionId);
350
- }
351
- }
352
-
353
- /// <summary>
354
- /// Callback from the MessageBus for handling TargetedMessages when this MessageHandler has subscribed - user code should generally never use this.
355
- /// </summary>
356
- /// <note>
357
- /// TargetedMessage refers to those that are intended for the GameObject that owns this MessageHandler.
358
- /// </note>
359
- /// <param name="target">Target Id the message is for.</param>
360
- /// <param name="message">Message to handle.</param>
361
- /// <param name="messageBus">The specific MessageBus to use.</param>
362
- /// <param name="priority">Priority at which to run the handlers.</param>
363
- public void HandleTargeted<TMessage>(
364
- ref InstanceId target,
365
- ref TMessage message,
366
- IMessageBus messageBus,
367
- int priority
368
- )
369
- where TMessage : ITargetedMessage
370
- {
371
- if (!active)
330
+ HandlerActionCache<FastHandler<IUntargetedMessage>> fastCache = handler.GetGlobalCache<
331
+ FastHandler<IUntargetedMessage>
332
+ >(TypedGlobalSlotIndex.UntargetedFast);
333
+ if (fastCache != null)
372
334
  {
373
- return;
335
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
374
336
  }
375
-
376
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
337
+ HandlerActionCache<Action<IUntargetedMessage>> cache = handler.GetGlobalCache<
338
+ Action<IUntargetedMessage>
339
+ >(TypedGlobalSlotIndex.UntargetedDefault);
340
+ if (cache != null)
377
341
  {
378
- long emissionId = messageBus.EmissionId;
379
- handler.HandleTargeted(ref target, ref message, priority, emissionId);
342
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
380
343
  }
381
344
  }
382
345
 
383
346
  /// <summary>
384
- /// Callback from the MessageBus for handling TargetedMessages without targeting when this MessageHandler has subscribed - user code should generally never use this.
347
+ /// Pre-freezes this handler's GlobalAcceptAll targeted caches for this emission.
385
348
  /// </summary>
386
- /// <note>
387
- /// Any TargetedMessage.
388
- /// </note>
389
- /// <param name="target">Target Id the message is for.</param>
390
- /// <param name="message">Message to handle.</param>
391
- /// <param name="messageBus">The specific MessageBus to use.</param>
392
- /// <param name="priority">Priority at which to run the handlers.</param>
393
- public void HandleTargetedWithoutTargeting<TMessage>(
394
- ref InstanceId target,
395
- ref TMessage message,
396
- IMessageBus messageBus,
397
- int priority
398
- )
399
- where TMessage : ITargetedMessage
349
+ internal void PrefreezeGlobalTargetedForEmission(long emissionId, IMessageBus messageBus)
400
350
  {
401
- if (!active)
351
+ if (!GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
402
352
  {
403
353
  return;
404
354
  }
405
355
 
406
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
407
- {
408
- long emissionId = messageBus.EmissionId;
409
- handler.HandleTargetedWithoutTargeting(
410
- ref target,
411
- ref message,
412
- priority,
413
- emissionId
356
+ HandlerActionCache<FastHandlerWithContext<ITargetedMessage>> fastCache =
357
+ handler.GetGlobalCache<FastHandlerWithContext<ITargetedMessage>>(
358
+ TypedGlobalSlotIndex.TargetedFast
414
359
  );
415
- }
416
- }
417
-
418
- /// <summary>
419
- /// Callback from the MessageBus for post-processing TargetedMessages when this MessageHandler has subscribed - user code should generally never use this.
420
- /// </summary>
421
- /// <note>
422
- /// TargetedMessage refers to those that are intended for the GameObject that owns this MessageHandler.
423
- /// </note>
424
- /// <param name="target">Target Id the message is for.</param>
425
- /// <param name="message">Message to handle.</param>
426
- /// <param name="messageBus">The specific MessageBus to use.</param>
427
- /// <param name="priority">Priority at which to run the handlers.</param>
428
- public void HandleTargetedPostProcessing<TMessage>(
429
- ref InstanceId target,
430
- ref TMessage message,
431
- IMessageBus messageBus,
432
- int priority
433
- )
434
- where TMessage : ITargetedMessage
435
- {
436
- if (!active)
360
+ if (fastCache != null)
437
361
  {
438
- return;
362
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
439
363
  }
440
-
441
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
364
+ HandlerActionCache<Action<InstanceId, ITargetedMessage>> cache = handler.GetGlobalCache<
365
+ Action<InstanceId, ITargetedMessage>
366
+ >(TypedGlobalSlotIndex.TargetedDefault);
367
+ if (cache != null)
442
368
  {
443
- long emissionId = messageBus.EmissionId;
444
- handler.HandleTargetedPostProcessing(ref target, ref message, priority, emissionId);
369
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
445
370
  }
446
371
  }
447
372
 
448
373
  /// <summary>
449
- /// Callback from the MessageBus for post-processing TargetedMessages when this MessageHandler has subscribed - user code should generally never use this.
374
+ /// Pre-freezes this handler's GlobalAcceptAll broadcast caches for this emission.
450
375
  /// </summary>
451
- /// <note>
452
- /// TargetedMessage refers to those that are intended for the GameObject that owns this MessageHandler.
453
- /// </note>
454
- /// <param name="target">Target Id the message is for.</param>
455
- /// <param name="message">Message to handle.</param>
456
- /// <param name="messageBus">The specific MessageBus to use.</param>
457
- /// <param name="priority">Priority at which to run the handlers.</param>
458
- public void HandleTargetedWithoutTargetingPostProcessing<TMessage>(
459
- ref InstanceId target,
460
- ref TMessage message,
461
- IMessageBus messageBus,
462
- int priority
463
- )
464
- where TMessage : ITargetedMessage
376
+ internal void PrefreezeGlobalBroadcastForEmission(long emissionId, IMessageBus messageBus)
465
377
  {
466
- if (!active)
378
+ if (!GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
467
379
  {
468
380
  return;
469
381
  }
470
382
 
471
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
472
- {
473
- long emissionId = messageBus.EmissionId;
474
- handler.HandleTargetedWithoutTargetingPostProcessing(
475
- ref target,
476
- ref message,
477
- priority,
478
- emissionId
383
+ HandlerActionCache<FastHandlerWithContext<IBroadcastMessage>> fastCache =
384
+ handler.GetGlobalCache<FastHandlerWithContext<IBroadcastMessage>>(
385
+ TypedGlobalSlotIndex.BroadcastFast
479
386
  );
480
- }
481
- }
482
-
483
- /// <summary>
484
- /// Callback from the MessageBus for handling SourcedBroadcastMessages - user code should generally never use this.
485
- /// </summary>
486
- /// <note>
487
- /// SourcedBroadcastMessages generally refer to those that are sourced from the GameObject that owns this MessageHandler.
488
- /// </note>
489
- /// <param name="source">Source Id the broadcast message is from.</param>
490
- /// <param name="message">Message to handle</param>
491
- /// <param name="messageBus">The specific MessageBus to use.</param>
492
- /// <param name="priority">Priority at which to run the handlers.</param>
493
- public void HandleSourcedBroadcast<TMessage>(
494
- ref InstanceId source,
495
- ref TMessage message,
496
- IMessageBus messageBus,
497
- int priority
498
- )
499
- where TMessage : IBroadcastMessage
500
- {
501
- if (!active)
387
+ if (fastCache != null)
502
388
  {
503
- return;
389
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
504
390
  }
505
-
506
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
391
+ HandlerActionCache<Action<InstanceId, IBroadcastMessage>> cache =
392
+ handler.GetGlobalCache<Action<InstanceId, IBroadcastMessage>>(
393
+ TypedGlobalSlotIndex.BroadcastDefault
394
+ );
395
+ if (cache != null)
507
396
  {
508
- long emissionId = messageBus.EmissionId;
509
- handler.HandleSourcedBroadcast(ref source, ref message, priority, emissionId);
397
+ _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
510
398
  }
511
399
  }
512
400
 
513
401
  /// <summary>
514
- /// Callback from the MessageBus for handling SourcedBroadcastMessages without source - user code should generally never use this.
402
+ /// Callback from the MessageBus for handling Messages when this MessageHandler has subscribed to GlobalAcceptAll - user code should generally never use this.
515
403
  /// </summary>
516
- /// <note>
517
- /// Any SourcedBroadcastMessages.
518
- /// </note>
519
- /// <param name="source">Source Id the broadcast message is from.</param>
520
- /// <param name="message">Message to handle</param>
404
+ /// <param name="target">Target of the message.</param>
405
+ /// <param name="message">Message to handle.</param>
521
406
  /// <param name="messageBus">The specific MessageBus to use.</param>
522
- /// <param name="priority">Priority at which to run the handlers.</param>
523
- public void HandleSourcedBroadcastWithoutSource<TMessage>(
524
- ref InstanceId source,
525
- ref TMessage message,
526
- IMessageBus messageBus,
527
- int priority
407
+ internal void HandleGlobalTargetedMessage(
408
+ ref InstanceId target,
409
+ ref ITargetedMessage message,
410
+ IMessageBus messageBus
528
411
  )
529
- where TMessage : IBroadcastMessage
530
412
  {
531
413
  if (!active)
532
414
  {
533
415
  return;
534
416
  }
535
417
 
536
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
418
+ // Use the "IMessage" explicitly to indicate global messages, allowing us to multipurpose a single dictionary
419
+ if (GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
537
420
  {
538
421
  long emissionId = messageBus.EmissionId;
539
- handler.HandleSourcedBroadcastWithoutSource(
540
- ref source,
541
- ref message,
542
- priority,
543
- emissionId
544
- );
422
+ handler.HandleGlobalTargeted(ref target, ref message, emissionId);
545
423
  }
546
424
  }
547
425
 
548
426
  /// <summary>
549
- /// Callback from the MessageBus for handling SourcedBroadcastPostProcessing - user code should generally never use this.
427
+ /// Callback from the MessageBus for handling Messages when this MessageHandler has subscribed to GlobalAcceptAll - user code should generally never use this.
550
428
  /// </summary>
551
- /// <note>
552
- /// SourcedBroadcastMessages generally refer to those that are sourced from the GameObject that owns this MessageHandler.
553
- /// </note>
554
- /// <param name="source">Source Id the broadcast message is from.</param>
555
- /// <param name="message">Message to handle</param>
429
+ /// <param name="source">Source that this message is from.</param>
430
+ /// <param name="message">Message to handle.</param>
556
431
  /// <param name="messageBus">The specific MessageBus to use.</param>
557
- /// <param name="priority">Priority at which to run the handlers.</param>
558
- public void HandleSourcedBroadcastPostProcessing<TMessage>(
432
+ internal void HandleGlobalSourcedBroadcastMessage(
559
433
  ref InstanceId source,
560
- ref TMessage message,
561
- IMessageBus messageBus,
562
- int priority
434
+ ref IBroadcastMessage message,
435
+ IMessageBus messageBus
563
436
  )
564
- where TMessage : IBroadcastMessage
565
437
  {
566
438
  if (!active)
567
439
  {
568
440
  return;
569
441
  }
570
442
 
571
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
443
+ // Use the "IMessage" explicitly to indicate global messages, allowing us to multipurpose a single dictionary
444
+ if (GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
572
445
  {
573
446
  long emissionId = messageBus.EmissionId;
574
- handler.HandleSourcedBroadcastPostProcessing(
575
- ref source,
576
- ref message,
577
- priority,
578
- emissionId
579
- );
447
+ handler.HandleGlobalBroadcast(ref source, ref message, emissionId);
580
448
  }
581
449
  }
582
450
 
583
451
  /// <summary>
584
- /// Callback from the MessageBus for handling SourcedBroadcastPostProcessing - user code should generally never use this.
452
+ /// Reference-identity teardown base used by token lifecycle queues. The common
453
+ /// token path is implemented by the token's existing Registration object with
454
+ /// value-type state returned from MessageHandler; concrete wrappers below are
455
+ /// reserved for direct handler compatibility and overlapping retry spills.
585
456
  /// </summary>
586
- /// <note>
587
- /// SourcedBroadcastMessages generally refer to those that are sourced from the GameObject that owns this MessageHandler.
588
- /// </note>
589
- /// <param name="source">Source Id the broadcast message is from.</param>
590
- /// <param name="message">Message to handle</param>
591
- /// <param name="messageBus">The specific MessageBus to use.</param>
592
- /// <param name="priority">Priority at which to run the handlers.</param>
593
- public void HandleSourcedBroadcastWithoutSourcePostProcessing<TMessage>(
594
- ref InstanceId source,
595
- ref TMessage message,
596
- IMessageBus messageBus,
597
- int priority
598
- )
599
- where TMessage : IBroadcastMessage
457
+ /// <remarks>
458
+ /// The implicit conversion to <see cref="Action"/> exists so the
459
+ /// handful of internal callers (contract tests through the
460
+ /// bus&lt;-&gt;handler boundary) that stored the old <see cref="Action"/>
461
+ /// continue to compile unchanged; it allocates a single delegate ONLY
462
+ /// when a caller explicitly requests the <see cref="Action"/> form, and
463
+ /// is never used on the token's common storage path.
464
+ /// </remarks>
465
+ internal abstract class HandlerDeregistration
600
466
  {
601
- if (!active)
602
- {
603
- return;
604
- }
467
+ internal abstract void Deregister();
605
468
 
606
- if (GetHandlerForType(messageBus, out TypedHandler<TMessage> handler))
469
+ public static implicit operator Action(HandlerDeregistration deregistration)
607
470
  {
608
- long emissionId = messageBus.EmissionId;
609
- handler.HandleBroadcastWithoutSourcePostProcessing(
610
- ref source,
611
- ref message,
612
- priority,
613
- emissionId
614
- );
471
+ return deregistration == null ? null : deregistration.Deregister;
615
472
  }
616
473
  }
617
474
 
618
475
  /// <summary>
619
- /// Callback from the MessageBus for handling Messages when this MessageHandler has subscribed to GlobalAcceptAll - user code should generally never use this.
476
+ /// Teardown for the interceptor facades: the bus-side registration is
477
+ /// the only thing to undo (there is no typed-handler cache entry).
478
+ /// Mirrors the old <c>() =&gt; messageBus.Deregister&lt;T&gt;(in registration)</c>.
620
479
  /// </summary>
621
- /// <param name="message">Message to handle.</param>
622
- /// <param name="messageBus">The specific MessageBus to use.</param>
623
- public void HandleGlobalUntargetedMessage(
624
- ref IUntargetedMessage message,
625
- IMessageBus messageBus
626
- )
480
+ internal readonly struct InterceptorDeregistrationState<T>
481
+ where T : IMessage
627
482
  {
628
- if (!active)
629
- {
630
- return;
631
- }
483
+ private readonly IMessageBus _messageBus;
484
+ private readonly MessageBusRegistration _registration;
632
485
 
633
- // Use the "IMessage" explicitly to indicate global messages, allowing us to multipurpose a single dictionary
634
- if (GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
486
+ internal InterceptorDeregistrationState(
487
+ IMessageBus messageBus,
488
+ in MessageBusRegistration registration
489
+ )
635
490
  {
636
- long emissionId = messageBus.EmissionId;
637
- handler.HandleGlobalUntargeted(ref message, emissionId);
491
+ _messageBus = messageBus;
492
+ _registration = registration;
638
493
  }
639
- }
640
494
 
641
- /// <summary>
642
- /// Pre-freezes this handler's GlobalAcceptAll untargeted caches for this emission.
643
- /// </summary>
644
- internal void PrefreezeGlobalUntargetedForEmission(long emissionId, IMessageBus messageBus)
645
- {
646
- if (!GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
495
+ internal void Deregister()
647
496
  {
648
- return;
497
+ _messageBus.Deregister<T>(in _registration);
649
498
  }
650
499
 
651
- HandlerActionCache<FastHandler<IUntargetedMessage>> fastCache = handler.GetGlobalCache<
652
- FastHandler<IUntargetedMessage>
653
- >(TypedGlobalSlotIndex.UntargetedFast);
654
- if (fastCache != null)
500
+ public static implicit operator HandlerDeregistration(
501
+ InterceptorDeregistrationState<T> state
502
+ )
655
503
  {
656
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
504
+ return new InterceptorDeregistration<T>(state);
657
505
  }
658
- HandlerActionCache<Action<IUntargetedMessage>> cache = handler.GetGlobalCache<
659
- Action<IUntargetedMessage>
660
- >(TypedGlobalSlotIndex.UntargetedDefault);
661
- if (cache != null)
506
+
507
+ public static implicit operator Action(InterceptorDeregistrationState<T> state)
662
508
  {
663
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
509
+ InterceptorDeregistration<T> deregistration = new(state);
510
+ return deregistration.Deregister;
664
511
  }
665
512
  }
666
513
 
667
- /// <summary>
668
- /// Pre-freezes this handler's GlobalAcceptAll targeted caches for this emission.
669
- /// </summary>
670
- internal void PrefreezeGlobalTargetedForEmission(long emissionId, IMessageBus messageBus)
514
+ internal sealed class InterceptorDeregistration<T> : HandlerDeregistration
515
+ where T : IMessage
671
516
  {
672
- if (!GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
673
- {
674
- return;
675
- }
517
+ private readonly InterceptorDeregistrationState<T> _state;
676
518
 
677
- HandlerActionCache<FastHandlerWithContext<ITargetedMessage>> fastCache =
678
- handler.GetGlobalCache<FastHandlerWithContext<ITargetedMessage>>(
679
- TypedGlobalSlotIndex.TargetedFast
680
- );
681
- if (fastCache != null)
519
+ internal InterceptorDeregistration(InterceptorDeregistrationState<T> state)
682
520
  {
683
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
521
+ _state = state;
684
522
  }
685
- HandlerActionCache<Action<InstanceId, ITargetedMessage>> cache = handler.GetGlobalCache<
686
- Action<InstanceId, ITargetedMessage>
687
- >(TypedGlobalSlotIndex.TargetedDefault);
688
- if (cache != null)
523
+
524
+ internal override void Deregister()
689
525
  {
690
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
526
+ _state.Deregister();
691
527
  }
692
528
  }
693
529
 
694
530
  /// <summary>
695
- /// Pre-freezes this handler's GlobalAcceptAll broadcast caches for this emission.
531
+ /// Value teardown for one typed global handler slot. Captures the old closure's
532
+ /// reset/slot guards, erased cache identity, refcount key, and touch/live-count state.
696
533
  /// </summary>
697
- internal void PrefreezeGlobalBroadcastForEmission(long emissionId, IMessageBus messageBus)
534
+ internal readonly struct GlobalHandlerDeregistrationState
698
535
  {
699
- if (!GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
536
+ private readonly IMessageBus _messageBus;
537
+ private readonly long _resetGeneration;
538
+ private readonly TypedGlobalSlot _slot;
539
+ private readonly long _slotVersion;
540
+ private readonly IHandlerActionCache _cache;
541
+ private readonly object _originalHandler;
542
+
543
+ internal GlobalHandlerDeregistrationState(
544
+ IMessageBus messageBus,
545
+ long resetGeneration,
546
+ TypedGlobalSlot slot,
547
+ long slotVersion,
548
+ IHandlerActionCache cache,
549
+ object originalHandler
550
+ )
700
551
  {
701
- return;
552
+ _messageBus = messageBus;
553
+ _resetGeneration = resetGeneration;
554
+ _slot = slot;
555
+ _slotVersion = slotVersion;
556
+ _cache = cache;
557
+ _originalHandler = originalHandler;
702
558
  }
703
559
 
704
- HandlerActionCache<FastHandlerWithContext<IBroadcastMessage>> fastCache =
705
- handler.GetGlobalCache<FastHandlerWithContext<IBroadcastMessage>>(
706
- TypedGlobalSlotIndex.BroadcastFast
707
- );
708
- if (fastCache != null)
709
- {
710
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(fastCache, emissionId);
711
- }
712
- HandlerActionCache<Action<InstanceId, IBroadcastMessage>> cache =
713
- handler.GetGlobalCache<Action<InstanceId, IBroadcastMessage>>(
714
- TypedGlobalSlotIndex.BroadcastDefault
715
- );
716
- if (cache != null)
560
+ internal void Deregister()
717
561
  {
718
- _ = TypedHandler<IMessage>.GetOrAddNewHandlerStack(cache, emissionId);
562
+ if (
563
+ !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
564
+ _messageBus,
565
+ _resetGeneration
566
+ )
567
+ || _slot.version != _slotVersion
568
+ || !_cache.ContainsEntry(_originalHandler)
569
+ )
570
+ {
571
+ return;
572
+ }
573
+
574
+ _cache.BumpVersion();
575
+ _slot.lastTouchTicks =
576
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(_messageBus);
577
+ _ = _cache.DeregisterEntry(_originalHandler, out bool wasLastForEntry);
578
+ if (wasLastForEntry)
579
+ {
580
+ _slot.liveCount--;
581
+ }
719
582
  }
720
583
  }
721
584
 
722
585
  /// <summary>
723
- /// Callback from the MessageBus for handling Messages when this MessageHandler has subscribed to GlobalAcceptAll - user code should generally never use this.
586
+ /// Composite teardown for the global accept-all registration: undoes the
587
+ /// bus-side global registration then the three typed sub-handler
588
+ /// deregistrations, preserving the exact order of the old closure
589
+ /// (<c>messageBus.Deregister&lt;IMessage&gt;(...); untargeted(); targeted(); broadcast();</c>).
724
590
  /// </summary>
725
- /// <param name="target">Target of the message.</param>
726
- /// <param name="message">Message to handle.</param>
727
- /// <param name="messageBus">The specific MessageBus to use.</param>
728
- public void HandleGlobalTargetedMessage(
729
- ref InstanceId target,
730
- ref ITargetedMessage message,
731
- IMessageBus messageBus
732
- )
591
+ internal readonly struct GlobalAcceptAllDeregistrationState
733
592
  {
734
- if (!active)
593
+ private readonly IMessageBus _messageBus;
594
+ private readonly MessageBusRegistration _messageBusRegistration;
595
+ private readonly GlobalHandlerDeregistrationState _untargetedDeregistration;
596
+ private readonly GlobalHandlerDeregistrationState _targetedDeregistration;
597
+ private readonly GlobalHandlerDeregistrationState _broadcastDeregistration;
598
+
599
+ internal GlobalAcceptAllDeregistrationState(
600
+ IMessageBus messageBus,
601
+ in MessageBusRegistration messageBusRegistration,
602
+ GlobalHandlerDeregistrationState untargetedDeregistration,
603
+ GlobalHandlerDeregistrationState targetedDeregistration,
604
+ GlobalHandlerDeregistrationState broadcastDeregistration
605
+ )
735
606
  {
736
- return;
607
+ _messageBus = messageBus;
608
+ _messageBusRegistration = messageBusRegistration;
609
+ _untargetedDeregistration = untargetedDeregistration;
610
+ _targetedDeregistration = targetedDeregistration;
611
+ _broadcastDeregistration = broadcastDeregistration;
737
612
  }
738
613
 
739
- // Use the "IMessage" explicitly to indicate global messages, allowing us to multipurpose a single dictionary
740
- if (GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
614
+ internal void Deregister()
741
615
  {
742
- long emissionId = messageBus.EmissionId;
743
- handler.HandleGlobalTargeted(ref target, ref message, emissionId);
616
+ _messageBus.Deregister<IMessage>(in _messageBusRegistration);
617
+ _untargetedDeregistration.Deregister();
618
+ _targetedDeregistration.Deregister();
619
+ _broadcastDeregistration.Deregister();
744
620
  }
745
- }
746
621
 
747
- /// <summary>
748
- /// Callback from the MessageBus for handling Messages when this MessageHandler has subscribed to GlobalAcceptAll - user code should generally never use this.
749
- /// </summary>
750
- /// <param name="source">Source that this message is from.</param>
751
- /// <param name="message">Message to handle.</param>
752
- /// <param name="messageBus">The specific MessageBus to use.</param>
753
- public void HandleGlobalSourcedBroadcastMessage(
754
- ref InstanceId source,
755
- ref IBroadcastMessage message,
756
- IMessageBus messageBus
757
- )
622
+ public static implicit operator HandlerDeregistration(
623
+ GlobalAcceptAllDeregistrationState state
624
+ )
625
+ {
626
+ return new GlobalAcceptAllDeregistration(state);
627
+ }
628
+
629
+ public static implicit operator Action(GlobalAcceptAllDeregistrationState state)
630
+ {
631
+ GlobalAcceptAllDeregistration deregistration = new(state);
632
+ return deregistration.Deregister;
633
+ }
634
+ }
635
+
636
+ internal sealed class GlobalAcceptAllDeregistration : HandlerDeregistration
758
637
  {
759
- if (!active)
638
+ private readonly GlobalAcceptAllDeregistrationState _state;
639
+
640
+ internal GlobalAcceptAllDeregistration(GlobalAcceptAllDeregistrationState state)
760
641
  {
761
- return;
642
+ _state = state;
762
643
  }
763
644
 
764
- // Use the "IMessage" explicitly to indicate global messages, allowing us to multipurpose a single dictionary
765
- if (GetHandlerForType(messageBus, out TypedHandler<IMessage> handler))
645
+ internal override void Deregister()
766
646
  {
767
- long emissionId = messageBus.EmissionId;
768
- handler.HandleGlobalBroadcast(ref source, ref message, emissionId);
647
+ _state.Deregister();
769
648
  }
770
649
  }
771
650
 
@@ -777,7 +656,7 @@ namespace DxMessaging.Core
777
656
  /// <param name="targetedMessageHandler">MessageHandler to accept all BroadcastMessages for all entities.</param>
778
657
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
779
658
  /// <returns>The de-registration action.</returns>
780
- public Action RegisterGlobalAcceptAll(
659
+ internal GlobalAcceptAllDeregistrationState RegisterGlobalAcceptAll(
781
660
  Action<IUntargetedMessage> originalUntargetedMessageHandler,
782
661
  Action<IUntargetedMessage> untargetedMessageHandler,
783
662
  Action<InstanceId, ITargetedMessage> originalTargetedMessageHandler,
@@ -788,40 +667,37 @@ namespace DxMessaging.Core
788
667
  )
789
668
  {
790
669
  messageBus = ResolveMessageBus(messageBus);
791
- Action messageBusDeregistration = messageBus.RegisterGlobalAcceptAll(this);
792
- TypedHandler<IMessage> typedHandler = GetOrCreateHandlerForType<IMessage>(messageBus);
793
-
794
- Action untargetedDeregistration = typedHandler.AddGlobalUntargetedHandler(
795
- originalUntargetedMessageHandler,
796
- untargetedMessageHandler,
797
- NullDeregistration,
798
- messageBus
799
- );
800
- Action targetedDeregistration = typedHandler.AddGlobalTargetedHandler(
801
- originalTargetedMessageHandler,
802
- targetedMessageHandler,
803
- NullDeregistration,
804
- messageBus
805
- );
806
- Action broadcastDeregistration = typedHandler.AddGlobalBroadcastHandler(
807
- originalBroadcastMessageHandler,
808
- broadcastMessageHandler,
809
- NullDeregistration,
810
- messageBus
670
+ MessageBusRegistration messageBusDeregistration = messageBus.RegisterGlobalAcceptAll(
671
+ this
811
672
  );
673
+ TypedHandler<IMessage> typedHandler = GetOrCreateHandlerForType<IMessage>(messageBus);
812
674
 
813
- return () =>
814
- {
815
- messageBusDeregistration?.Invoke();
816
- untargetedDeregistration();
817
- targetedDeregistration();
818
- broadcastDeregistration();
819
- };
675
+ GlobalHandlerDeregistrationState untargetedDeregistration =
676
+ typedHandler.AddGlobalUntargetedHandler(
677
+ originalUntargetedMessageHandler,
678
+ untargetedMessageHandler,
679
+ messageBus
680
+ );
681
+ GlobalHandlerDeregistrationState targetedDeregistration =
682
+ typedHandler.AddGlobalTargetedHandler(
683
+ originalTargetedMessageHandler,
684
+ targetedMessageHandler,
685
+ messageBus
686
+ );
687
+ GlobalHandlerDeregistrationState broadcastDeregistration =
688
+ typedHandler.AddGlobalBroadcastHandler(
689
+ originalBroadcastMessageHandler,
690
+ broadcastMessageHandler,
691
+ messageBus
692
+ );
820
693
 
821
- void NullDeregistration()
822
- {
823
- // No-op
824
- }
694
+ return new GlobalAcceptAllDeregistrationState(
695
+ messageBus,
696
+ in messageBusDeregistration,
697
+ untargetedDeregistration,
698
+ targetedDeregistration,
699
+ broadcastDeregistration
700
+ );
825
701
  }
826
702
 
827
703
  /// <summary>
@@ -832,7 +708,7 @@ namespace DxMessaging.Core
832
708
  /// <param name="targetedMessageHandler">MessageHandler to accept all BroadcastMessages for all entities.</param>
833
709
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
834
710
  /// <returns>The de-registration action.</returns>
835
- public Action RegisterGlobalAcceptAll(
711
+ internal GlobalAcceptAllDeregistrationState RegisterGlobalAcceptAll(
836
712
  FastHandler<IUntargetedMessage> originalUntargetedMessageHandler,
837
713
  FastHandler<IUntargetedMessage> untargetedMessageHandler,
838
714
  FastHandlerWithContext<ITargetedMessage> originalTargetedMessageHandler,
@@ -843,40 +719,37 @@ namespace DxMessaging.Core
843
719
  )
844
720
  {
845
721
  messageBus = ResolveMessageBus(messageBus);
846
- Action messageBusDeregistration = messageBus.RegisterGlobalAcceptAll(this);
847
- TypedHandler<IMessage> typedHandler = GetOrCreateHandlerForType<IMessage>(messageBus);
848
-
849
- Action untargetedDeregistration = typedHandler.AddGlobalUntargetedHandler(
850
- originalUntargetedMessageHandler,
851
- untargetedMessageHandler,
852
- NullDeregistration,
853
- messageBus
854
- );
855
- Action targetedDeregistration = typedHandler.AddGlobalTargetedHandler(
856
- originalTargetedMessageHandler,
857
- targetedMessageHandler,
858
- NullDeregistration,
859
- messageBus
860
- );
861
- Action broadcastDeregistration = typedHandler.AddGlobalBroadcastHandler(
862
- originalBroadcastMessageHandler,
863
- broadcastMessageHandler,
864
- NullDeregistration,
865
- messageBus
722
+ MessageBusRegistration messageBusDeregistration = messageBus.RegisterGlobalAcceptAll(
723
+ this
866
724
  );
725
+ TypedHandler<IMessage> typedHandler = GetOrCreateHandlerForType<IMessage>(messageBus);
867
726
 
868
- return () =>
869
- {
870
- messageBusDeregistration?.Invoke();
871
- untargetedDeregistration();
872
- targetedDeregistration();
873
- broadcastDeregistration();
874
- };
727
+ GlobalHandlerDeregistrationState untargetedDeregistration =
728
+ typedHandler.AddGlobalUntargetedHandler(
729
+ originalUntargetedMessageHandler,
730
+ untargetedMessageHandler,
731
+ messageBus
732
+ );
733
+ GlobalHandlerDeregistrationState targetedDeregistration =
734
+ typedHandler.AddGlobalTargetedHandler(
735
+ originalTargetedMessageHandler,
736
+ targetedMessageHandler,
737
+ messageBus
738
+ );
739
+ GlobalHandlerDeregistrationState broadcastDeregistration =
740
+ typedHandler.AddGlobalBroadcastHandler(
741
+ originalBroadcastMessageHandler,
742
+ broadcastMessageHandler,
743
+ messageBus
744
+ );
875
745
 
876
- void NullDeregistration()
877
- {
878
- // No-op
879
- }
746
+ return new GlobalAcceptAllDeregistrationState(
747
+ messageBus,
748
+ in messageBusDeregistration,
749
+ untargetedDeregistration,
750
+ targetedDeregistration,
751
+ broadcastDeregistration
752
+ );
880
753
  }
881
754
 
882
755
  /// <summary>
@@ -888,7 +761,7 @@ namespace DxMessaging.Core
888
761
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
889
762
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
890
763
  /// <returns>The de-registration action.</returns>
891
- public Action RegisterTargetedMessageHandler<T>(
764
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedMessageHandler<T>(
892
765
  InstanceId target,
893
766
  Action<T> originalHandler,
894
767
  Action<T> messageHandler,
@@ -898,7 +771,7 @@ namespace DxMessaging.Core
898
771
  where T : ITargetedMessage
899
772
  {
900
773
  messageBus = ResolveMessageBus(messageBus);
901
- Action messageBusDeregistration = messageBus.RegisterTargeted<T>(
774
+ MessageBusRegistration messageBusDeregistration = messageBus.RegisterTargeted<T>(
902
775
  target,
903
776
  this,
904
777
  priority: priority
@@ -914,6 +787,39 @@ namespace DxMessaging.Core
914
787
  );
915
788
  }
916
789
 
790
+ /// <summary>
791
+ /// Registers a targeted handler whose diagnostics-augmented by-ref flat
792
+ /// invoker was already built by the caller (the registration token), so the
793
+ /// default slot stores a single closure instead of an <see cref="Action{T}"/>
794
+ /// wrapper plus a separately allocated FastHandler adapter.
795
+ /// <paramref name="originalHandler"/> stays the dedup/identity key.
796
+ /// </summary>
797
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedMessageHandler<T>(
798
+ InstanceId target,
799
+ Action<T> originalHandler,
800
+ FastHandler<T> flatInvoker,
801
+ int priority = 0,
802
+ IMessageBus messageBus = null
803
+ )
804
+ where T : ITargetedMessage
805
+ {
806
+ messageBus = ResolveMessageBus(messageBus);
807
+ MessageBusRegistration messageBusDeregistration = messageBus.RegisterTargeted<T>(
808
+ target,
809
+ this,
810
+ priority: priority
811
+ );
812
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
813
+ return typedHandler.AddTargetedHandler(
814
+ target,
815
+ originalHandler,
816
+ flatInvoker,
817
+ messageBusDeregistration,
818
+ priority,
819
+ messageBus
820
+ );
821
+ }
822
+
917
823
  /// <summary>
918
824
  /// Registers this MessageHandler to accept fast TargetedMessages via the MessageBus, properly handling deregistration.
919
825
  /// </summary>
@@ -923,7 +829,7 @@ namespace DxMessaging.Core
923
829
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
924
830
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
925
831
  /// <returns>The de-registration action.</returns>
926
- public Action RegisterTargetedMessageHandler<T>(
832
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedMessageHandler<T>(
927
833
  InstanceId target,
928
834
  FastHandler<T> originalHandler,
929
835
  FastHandler<T> messageHandler,
@@ -933,7 +839,7 @@ namespace DxMessaging.Core
933
839
  where T : ITargetedMessage
934
840
  {
935
841
  messageBus = ResolveMessageBus(messageBus);
936
- Action messageBusDeregistration = messageBus.RegisterTargeted<T>(
842
+ MessageBusRegistration messageBusDeregistration = messageBus.RegisterTargeted<T>(
937
843
  target,
938
844
  this,
939
845
  priority: priority
@@ -958,7 +864,7 @@ namespace DxMessaging.Core
958
864
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
959
865
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
960
866
  /// <returns>The de-registration action.</returns>
961
- public Action RegisterTargetedPostProcessor<T>(
867
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedPostProcessor<T>(
962
868
  InstanceId target,
963
869
  Action<T> originalHandler,
964
870
  Action<T> messageHandler,
@@ -968,11 +874,8 @@ namespace DxMessaging.Core
968
874
  where T : ITargetedMessage
969
875
  {
970
876
  messageBus = ResolveMessageBus(messageBus);
971
- Action messageBusDeregistration = messageBus.RegisterTargetedPostProcessor<T>(
972
- target,
973
- this,
974
- priority
975
- );
877
+ MessageBusRegistration messageBusDeregistration =
878
+ messageBus.RegisterTargetedPostProcessor<T>(target, this, priority);
976
879
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
977
880
  return typedHandler.AddTargetedPostProcessor(
978
881
  target,
@@ -993,7 +896,7 @@ namespace DxMessaging.Core
993
896
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
994
897
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
995
898
  /// <returns>The de-registration action.</returns>
996
- public Action RegisterTargetedPostProcessor<T>(
899
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedPostProcessor<T>(
997
900
  InstanceId target,
998
901
  FastHandler<T> originalHandler,
999
902
  FastHandler<T> messageHandler,
@@ -1003,16 +906,43 @@ namespace DxMessaging.Core
1003
906
  where T : ITargetedMessage
1004
907
  {
1005
908
  messageBus = ResolveMessageBus(messageBus);
1006
- Action messageBusDeregistration = messageBus.RegisterTargetedPostProcessor<T>(
909
+ MessageBusRegistration messageBusDeregistration =
910
+ messageBus.RegisterTargetedPostProcessor<T>(target, this, priority: priority);
911
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
912
+ return typedHandler.AddTargetedPostProcessor(
1007
913
  target,
1008
- this,
1009
- priority: priority
914
+ originalHandler,
915
+ messageHandler,
916
+ messageBusDeregistration,
917
+ priority,
918
+ messageBus
1010
919
  );
920
+ }
921
+
922
+ /// <summary>
923
+ /// Registers a targeted post-processor whose diagnostics-augmented by-ref flat
924
+ /// invoker was already built by the caller (the registration token), so the
925
+ /// default slot stores a single closure instead of an <see cref="Action{T}"/>
926
+ /// wrapper plus a separately allocated FastHandler adapter.
927
+ /// <paramref name="originalHandler"/> stays the dedup/identity key.
928
+ /// </summary>
929
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedPostProcessor<T>(
930
+ InstanceId target,
931
+ Action<T> originalHandler,
932
+ FastHandler<T> flatInvoker,
933
+ int priority = 0,
934
+ IMessageBus messageBus = null
935
+ )
936
+ where T : ITargetedMessage
937
+ {
938
+ messageBus = ResolveMessageBus(messageBus);
939
+ MessageBusRegistration messageBusDeregistration =
940
+ messageBus.RegisterTargetedPostProcessor<T>(target, this, priority);
1011
941
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1012
942
  return typedHandler.AddTargetedPostProcessor(
1013
943
  target,
1014
944
  originalHandler,
1015
- messageHandler,
945
+ flatInvoker,
1016
946
  messageBusDeregistration,
1017
947
  priority,
1018
948
  messageBus
@@ -1027,7 +957,7 @@ namespace DxMessaging.Core
1027
957
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1028
958
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1029
959
  /// <returns>The de-registration action.</returns>
1030
- public Action RegisterTargetedWithoutTargetingPostProcessor<T>(
960
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedWithoutTargetingPostProcessor<T>(
1031
961
  Action<InstanceId, T> originalHandler,
1032
962
  Action<InstanceId, T> messageHandler,
1033
963
  int priority = 0,
@@ -1036,7 +966,7 @@ namespace DxMessaging.Core
1036
966
  where T : ITargetedMessage
1037
967
  {
1038
968
  messageBus = ResolveMessageBus(messageBus);
1039
- Action messageBusDeregistration =
969
+ MessageBusRegistration messageBusDeregistration =
1040
970
  messageBus.RegisterTargetedWithoutTargetingPostProcessor<T>(
1041
971
  priority: priority,
1042
972
  messageHandler: this
@@ -1059,7 +989,7 @@ namespace DxMessaging.Core
1059
989
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1060
990
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1061
991
  /// <returns>The de-registration action.</returns>
1062
- public Action RegisterTargetedWithoutTargetingPostProcessor<T>(
992
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedWithoutTargetingPostProcessor<T>(
1063
993
  FastHandlerWithContext<T> originalHandler,
1064
994
  FastHandlerWithContext<T> messageHandler,
1065
995
  int priority = 0,
@@ -1068,7 +998,7 @@ namespace DxMessaging.Core
1068
998
  where T : ITargetedMessage
1069
999
  {
1070
1000
  messageBus = ResolveMessageBus(messageBus);
1071
- Action messageBusDeregistration =
1001
+ MessageBusRegistration messageBusDeregistration =
1072
1002
  messageBus.RegisterTargetedWithoutTargetingPostProcessor<T>(
1073
1003
  priority: priority,
1074
1004
  messageHandler: this
@@ -1083,6 +1013,38 @@ namespace DxMessaging.Core
1083
1013
  );
1084
1014
  }
1085
1015
 
1016
+ /// <summary>
1017
+ /// Registers a targeted (without-targeting) post-processor whose
1018
+ /// diagnostics-augmented by-ref context flat invoker was already built by the
1019
+ /// caller (the registration token), so the default slot stores a single closure
1020
+ /// instead of an <see cref="Action{T1, T2}"/> wrapper plus a separately
1021
+ /// allocated FastHandlerWithContext adapter. <paramref name="originalHandler"/>
1022
+ /// stays the dedup/identity key.
1023
+ /// </summary>
1024
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedWithoutTargetingPostProcessor<T>(
1025
+ Action<InstanceId, T> originalHandler,
1026
+ FastHandlerWithContext<T> flatInvoker,
1027
+ int priority = 0,
1028
+ IMessageBus messageBus = null
1029
+ )
1030
+ where T : ITargetedMessage
1031
+ {
1032
+ messageBus = ResolveMessageBus(messageBus);
1033
+ MessageBusRegistration messageBusDeregistration =
1034
+ messageBus.RegisterTargetedWithoutTargetingPostProcessor<T>(
1035
+ priority: priority,
1036
+ messageHandler: this
1037
+ );
1038
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1039
+ return typedHandler.AddTargetedWithoutTargetingPostProcessor(
1040
+ originalHandler,
1041
+ flatInvoker,
1042
+ messageBusDeregistration,
1043
+ priority,
1044
+ messageBus
1045
+ );
1046
+ }
1047
+
1086
1048
  /// <summary>
1087
1049
  /// Registers this MessageHandler to accept TargetedMessages without Targeting via the MessageBus, properly handling deregistration.
1088
1050
  /// </summary>
@@ -1091,7 +1053,7 @@ namespace DxMessaging.Core
1091
1053
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1092
1054
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1093
1055
  /// <returns>The de-registration action.</returns>
1094
- public Action RegisterTargetedWithoutTargeting<T>(
1056
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedWithoutTargeting<T>(
1095
1057
  Action<InstanceId, T> originalHandler,
1096
1058
  Action<InstanceId, T> messageHandler,
1097
1059
  int priority = 0,
@@ -1100,10 +1062,8 @@ namespace DxMessaging.Core
1100
1062
  where T : ITargetedMessage
1101
1063
  {
1102
1064
  messageBus = ResolveMessageBus(messageBus);
1103
- Action messageBusDeregistration = messageBus.RegisterTargetedWithoutTargeting<T>(
1104
- this,
1105
- priority: priority
1106
- );
1065
+ MessageBusRegistration messageBusDeregistration =
1066
+ messageBus.RegisterTargetedWithoutTargeting<T>(this, priority: priority);
1107
1067
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1108
1068
  return typedHandler.AddTargetedWithoutTargetingHandler(
1109
1069
  originalHandler,
@@ -1114,6 +1074,35 @@ namespace DxMessaging.Core
1114
1074
  );
1115
1075
  }
1116
1076
 
1077
+ /// <summary>
1078
+ /// Registers a targeted-without-targeting handler whose diagnostics-augmented
1079
+ /// by-ref-with-context flat invoker was already built by the caller, so the
1080
+ /// default slot stores a single closure instead of an
1081
+ /// <see cref="Action{InstanceId, T}"/> wrapper plus a separately allocated
1082
+ /// FastHandlerWithContext adapter. <paramref name="originalHandler"/> stays
1083
+ /// the dedup/identity key.
1084
+ /// </summary>
1085
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedWithoutTargeting<T>(
1086
+ Action<InstanceId, T> originalHandler,
1087
+ FastHandlerWithContext<T> flatInvoker,
1088
+ int priority = 0,
1089
+ IMessageBus messageBus = null
1090
+ )
1091
+ where T : ITargetedMessage
1092
+ {
1093
+ messageBus = ResolveMessageBus(messageBus);
1094
+ MessageBusRegistration messageBusDeregistration =
1095
+ messageBus.RegisterTargetedWithoutTargeting<T>(this, priority: priority);
1096
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1097
+ return typedHandler.AddTargetedWithoutTargetingHandler(
1098
+ originalHandler,
1099
+ flatInvoker,
1100
+ messageBusDeregistration,
1101
+ priority,
1102
+ messageBus
1103
+ );
1104
+ }
1105
+
1117
1106
  /// <summary>
1118
1107
  /// Registers this MessageHandler to accept fast TargetedMessages without Targeting via the MessageBus, properly handling deregistration.
1119
1108
  /// </summary>
@@ -1122,7 +1111,7 @@ namespace DxMessaging.Core
1122
1111
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1123
1112
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1124
1113
  /// <returns>The de-registration action.</returns>
1125
- public Action RegisterTargetedWithoutTargeting<T>(
1114
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterTargetedWithoutTargeting<T>(
1126
1115
  FastHandlerWithContext<T> originalHandler,
1127
1116
  FastHandlerWithContext<T> messageHandler,
1128
1117
  int priority = 0,
@@ -1131,10 +1120,8 @@ namespace DxMessaging.Core
1131
1120
  where T : ITargetedMessage
1132
1121
  {
1133
1122
  messageBus = ResolveMessageBus(messageBus);
1134
- Action messageBusDeregistration = messageBus.RegisterTargetedWithoutTargeting<T>(
1135
- this,
1136
- priority: priority
1137
- );
1123
+ MessageBusRegistration messageBusDeregistration =
1124
+ messageBus.RegisterTargetedWithoutTargeting<T>(this, priority: priority);
1138
1125
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1139
1126
  return typedHandler.AddTargetedWithoutTargetingHandler(
1140
1127
  originalHandler,
@@ -1153,7 +1140,7 @@ namespace DxMessaging.Core
1153
1140
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1154
1141
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1155
1142
  /// <returns>The de-registration action.</returns>
1156
- public Action RegisterUntargetedMessageHandler<T>(
1143
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterUntargetedMessageHandler<T>(
1157
1144
  Action<T> originalHandler,
1158
1145
  Action<T> messageHandler,
1159
1146
  int priority = 0,
@@ -1162,7 +1149,7 @@ namespace DxMessaging.Core
1162
1149
  where T : IUntargetedMessage
1163
1150
  {
1164
1151
  messageBus = ResolveMessageBus(messageBus);
1165
- Action messageBusDeregistration = messageBus.RegisterUntargeted<T>(
1152
+ MessageBusRegistration messageBusDeregistration = messageBus.RegisterUntargeted<T>(
1166
1153
  this,
1167
1154
  priority: priority
1168
1155
  );
@@ -1176,6 +1163,36 @@ namespace DxMessaging.Core
1176
1163
  );
1177
1164
  }
1178
1165
 
1166
+ /// <summary>
1167
+ /// Registers an untargeted handler whose diagnostics-augmented by-ref flat
1168
+ /// invoker was already built by the caller (the registration token), so the
1169
+ /// default slot stores a single closure instead of an <see cref="Action{T}"/>
1170
+ /// wrapper plus a separately allocated FastHandler adapter.
1171
+ /// <paramref name="originalHandler"/> stays the dedup/identity key.
1172
+ /// </summary>
1173
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterUntargetedMessageHandler<T>(
1174
+ Action<T> originalHandler,
1175
+ FastHandler<T> flatInvoker,
1176
+ int priority = 0,
1177
+ IMessageBus messageBus = null
1178
+ )
1179
+ where T : IUntargetedMessage
1180
+ {
1181
+ messageBus = ResolveMessageBus(messageBus);
1182
+ MessageBusRegistration messageBusDeregistration = messageBus.RegisterUntargeted<T>(
1183
+ this,
1184
+ priority: priority
1185
+ );
1186
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1187
+ return typedHandler.AddUntargetedHandler(
1188
+ originalHandler,
1189
+ flatInvoker,
1190
+ messageBusDeregistration,
1191
+ priority,
1192
+ messageBus
1193
+ );
1194
+ }
1195
+
1179
1196
  /// <summary>
1180
1197
  /// Registers this MessageHandler to accept fast UntargetedMessages via the MessageBus, properly handling deregistration.
1181
1198
  /// </summary>
@@ -1184,7 +1201,7 @@ namespace DxMessaging.Core
1184
1201
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1185
1202
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1186
1203
  /// <returns>The de-registration action.</returns>
1187
- public Action RegisterUntargetedMessageHandler<T>(
1204
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterUntargetedMessageHandler<T>(
1188
1205
  FastHandler<T> originalHandler,
1189
1206
  FastHandler<T> messageHandler,
1190
1207
  int priority = 0,
@@ -1193,7 +1210,7 @@ namespace DxMessaging.Core
1193
1210
  where T : IUntargetedMessage
1194
1211
  {
1195
1212
  messageBus = ResolveMessageBus(messageBus);
1196
- Action messageBusDeregistration = messageBus.RegisterUntargeted<T>(
1213
+ MessageBusRegistration messageBusDeregistration = messageBus.RegisterUntargeted<T>(
1197
1214
  this,
1198
1215
  priority: priority
1199
1216
  );
@@ -1215,7 +1232,7 @@ namespace DxMessaging.Core
1215
1232
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1216
1233
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1217
1234
  /// <returns>The de-registration action.</returns>
1218
- public Action RegisterUntargetedPostProcessor<T>(
1235
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterUntargetedPostProcessor<T>(
1219
1236
  Action<T> originalHandler,
1220
1237
  Action<T> messageHandler,
1221
1238
  int priority = 0,
@@ -1224,10 +1241,11 @@ namespace DxMessaging.Core
1224
1241
  where T : IUntargetedMessage
1225
1242
  {
1226
1243
  messageBus = ResolveMessageBus(messageBus);
1227
- Action messageBusDeregistration = messageBus.RegisterUntargetedPostProcessor<T>(
1228
- priority: priority,
1229
- messageHandler: this
1230
- );
1244
+ MessageBusRegistration messageBusDeregistration =
1245
+ messageBus.RegisterUntargetedPostProcessor<T>(
1246
+ priority: priority,
1247
+ messageHandler: this
1248
+ );
1231
1249
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1232
1250
  return typedHandler.AddUntargetedPostProcessor(
1233
1251
  originalHandler,
@@ -1246,7 +1264,7 @@ namespace DxMessaging.Core
1246
1264
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1247
1265
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1248
1266
  /// <returns>The de-registration action.</returns>
1249
- public Action RegisterUntargetedPostProcessor<T>(
1267
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterUntargetedPostProcessor<T>(
1250
1268
  FastHandler<T> originalHandler,
1251
1269
  FastHandler<T> messageHandler,
1252
1270
  int priority = 0,
@@ -1255,10 +1273,11 @@ namespace DxMessaging.Core
1255
1273
  where T : IUntargetedMessage
1256
1274
  {
1257
1275
  messageBus = ResolveMessageBus(messageBus);
1258
- Action messageBusDeregistration = messageBus.RegisterUntargetedPostProcessor<T>(
1259
- priority: priority,
1260
- messageHandler: this
1261
- );
1276
+ MessageBusRegistration messageBusDeregistration =
1277
+ messageBus.RegisterUntargetedPostProcessor<T>(
1278
+ priority: priority,
1279
+ messageHandler: this
1280
+ );
1262
1281
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1263
1282
  return typedHandler.AddUntargetedPostProcessor(
1264
1283
  originalHandler,
@@ -1278,7 +1297,7 @@ namespace DxMessaging.Core
1278
1297
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1279
1298
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1280
1299
  /// <returns>The de-registration action.</returns>
1281
- public Action RegisterSourcedBroadcastMessageHandler<T>(
1300
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastMessageHandler<T>(
1282
1301
  InstanceId source,
1283
1302
  Action<T> originalHandler,
1284
1303
  Action<T> messageHandler,
@@ -1288,11 +1307,8 @@ namespace DxMessaging.Core
1288
1307
  where T : IBroadcastMessage
1289
1308
  {
1290
1309
  messageBus = ResolveMessageBus(messageBus);
1291
- Action messageBusDeregistration = messageBus.RegisterSourcedBroadcast<T>(
1292
- source,
1293
- this,
1294
- priority: priority
1295
- );
1310
+ MessageBusRegistration messageBusDeregistration =
1311
+ messageBus.RegisterSourcedBroadcast<T>(source, this, priority: priority);
1296
1312
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1297
1313
 
1298
1314
  return typedHandler.AddSourcedBroadcastHandler(
@@ -1305,6 +1321,36 @@ namespace DxMessaging.Core
1305
1321
  );
1306
1322
  }
1307
1323
 
1324
+ /// <summary>
1325
+ /// Registers a sourced-broadcast handler whose diagnostics-augmented by-ref
1326
+ /// flat invoker was already built by the caller, so the default slot stores a
1327
+ /// single closure instead of an <see cref="Action{T}"/> wrapper plus a
1328
+ /// separately allocated FastHandler adapter.
1329
+ /// <paramref name="originalHandler"/> stays the dedup/identity key.
1330
+ /// </summary>
1331
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastMessageHandler<T>(
1332
+ InstanceId source,
1333
+ Action<T> originalHandler,
1334
+ FastHandler<T> flatInvoker,
1335
+ int priority = 0,
1336
+ IMessageBus messageBus = null
1337
+ )
1338
+ where T : IBroadcastMessage
1339
+ {
1340
+ messageBus = ResolveMessageBus(messageBus);
1341
+ MessageBusRegistration messageBusDeregistration =
1342
+ messageBus.RegisterSourcedBroadcast<T>(source, this, priority: priority);
1343
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1344
+ return typedHandler.AddSourcedBroadcastHandler(
1345
+ source,
1346
+ originalHandler,
1347
+ flatInvoker,
1348
+ messageBusDeregistration,
1349
+ priority,
1350
+ messageBus
1351
+ );
1352
+ }
1353
+
1308
1354
  /// <summary>
1309
1355
  /// Registers this MessageHandler to accept fast BroadcastMessages via their MessageBus, properly handling deregistration.
1310
1356
  /// </summary>
@@ -1314,7 +1360,7 @@ namespace DxMessaging.Core
1314
1360
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1315
1361
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1316
1362
  /// <returns>The de-registration action.</returns>
1317
- public Action RegisterSourcedBroadcastMessageHandler<T>(
1363
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastMessageHandler<T>(
1318
1364
  InstanceId source,
1319
1365
  FastHandler<T> originalHandler,
1320
1366
  FastHandler<T> messageHandler,
@@ -1324,11 +1370,8 @@ namespace DxMessaging.Core
1324
1370
  where T : IBroadcastMessage
1325
1371
  {
1326
1372
  messageBus = ResolveMessageBus(messageBus);
1327
- Action messageBusDeregistration = messageBus.RegisterSourcedBroadcast<T>(
1328
- source,
1329
- this,
1330
- priority: priority
1331
- );
1373
+ MessageBusRegistration messageBusDeregistration =
1374
+ messageBus.RegisterSourcedBroadcast<T>(source, this, priority: priority);
1332
1375
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1333
1376
  return typedHandler.AddSourcedBroadcastHandler(
1334
1377
  source,
@@ -1348,7 +1391,7 @@ namespace DxMessaging.Core
1348
1391
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1349
1392
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1350
1393
  /// <returns>The de-registration action.</returns>
1351
- public Action RegisterSourcedBroadcastWithoutSource<T>(
1394
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastWithoutSource<T>(
1352
1395
  Action<InstanceId, T> originalHandler,
1353
1396
  Action<InstanceId, T> messageHandler,
1354
1397
  int priority = 0,
@@ -1357,10 +1400,8 @@ namespace DxMessaging.Core
1357
1400
  where T : IBroadcastMessage
1358
1401
  {
1359
1402
  messageBus = ResolveMessageBus(messageBus);
1360
- Action messageBusDeregistration = messageBus.RegisterSourcedBroadcastWithoutSource<T>(
1361
- this,
1362
- priority: priority
1363
- );
1403
+ MessageBusRegistration messageBusDeregistration =
1404
+ messageBus.RegisterSourcedBroadcastWithoutSource<T>(this, priority: priority);
1364
1405
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1365
1406
  return typedHandler.AddSourcedBroadcastWithoutSourceHandler(
1366
1407
  originalHandler,
@@ -1371,6 +1412,35 @@ namespace DxMessaging.Core
1371
1412
  );
1372
1413
  }
1373
1414
 
1415
+ /// <summary>
1416
+ /// Registers a broadcast-without-source handler whose diagnostics-augmented
1417
+ /// by-ref-with-context flat invoker was already built by the caller, so the
1418
+ /// default slot stores a single closure instead of an
1419
+ /// <see cref="Action{InstanceId, T}"/> wrapper plus a separately allocated
1420
+ /// FastHandlerWithContext adapter. <paramref name="originalHandler"/> stays
1421
+ /// the dedup/identity key.
1422
+ /// </summary>
1423
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastWithoutSource<T>(
1424
+ Action<InstanceId, T> originalHandler,
1425
+ FastHandlerWithContext<T> flatInvoker,
1426
+ int priority = 0,
1427
+ IMessageBus messageBus = null
1428
+ )
1429
+ where T : IBroadcastMessage
1430
+ {
1431
+ messageBus = ResolveMessageBus(messageBus);
1432
+ MessageBusRegistration messageBusDeregistration =
1433
+ messageBus.RegisterSourcedBroadcastWithoutSource<T>(this, priority: priority);
1434
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1435
+ return typedHandler.AddSourcedBroadcastWithoutSourceHandler(
1436
+ originalHandler,
1437
+ flatInvoker,
1438
+ messageBusDeregistration,
1439
+ priority,
1440
+ messageBus
1441
+ );
1442
+ }
1443
+
1374
1444
  /// <summary>
1375
1445
  /// Registers this MessageHandler to accept fast BroadcastMessage regardless of source via their MessageBus, properly handling deregistration.
1376
1446
  /// </summary>
@@ -1379,7 +1449,7 @@ namespace DxMessaging.Core
1379
1449
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1380
1450
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1381
1451
  /// <returns>The de-registration action.</returns>
1382
- public Action RegisterSourcedBroadcastWithoutSource<T>(
1452
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastWithoutSource<T>(
1383
1453
  FastHandlerWithContext<T> originalHandler,
1384
1454
  FastHandlerWithContext<T> messageHandler,
1385
1455
  int priority = 0,
@@ -1388,10 +1458,8 @@ namespace DxMessaging.Core
1388
1458
  where T : IBroadcastMessage
1389
1459
  {
1390
1460
  messageBus = ResolveMessageBus(messageBus);
1391
- Action messageBusDeregistration = messageBus.RegisterSourcedBroadcastWithoutSource<T>(
1392
- this,
1393
- priority: priority
1394
- );
1461
+ MessageBusRegistration messageBusDeregistration =
1462
+ messageBus.RegisterSourcedBroadcastWithoutSource<T>(this, priority: priority);
1395
1463
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1396
1464
  return typedHandler.AddSourcedBroadcastWithoutSourceHandler(
1397
1465
  originalHandler,
@@ -1411,21 +1479,58 @@ namespace DxMessaging.Core
1411
1479
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1412
1480
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1413
1481
  /// <returns>The de-registration action.</returns>
1414
- public Action RegisterSourcedBroadcastPostProcessor<T>(
1482
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastPostProcessor<T>(
1483
+ InstanceId source,
1484
+ Action<T> originalHandler,
1485
+ Action<T> messageHandler,
1486
+ int priority = 0,
1487
+ IMessageBus messageBus = null
1488
+ )
1489
+ where T : IBroadcastMessage
1490
+ {
1491
+ messageBus = ResolveMessageBus(messageBus);
1492
+ MessageBusRegistration messageBusDeregistration =
1493
+ messageBus.RegisterBroadcastPostProcessor<T>(
1494
+ source,
1495
+ messageHandler: this,
1496
+ priority: priority
1497
+ );
1498
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1499
+ return typedHandler.AddBroadcastPostProcessor(
1500
+ source,
1501
+ originalHandler,
1502
+ messageHandler,
1503
+ messageBusDeregistration,
1504
+ priority,
1505
+ messageBus
1506
+ );
1507
+ }
1508
+
1509
+ /// <summary>
1510
+ /// Registers this MessageHandler to post processes fast BroadcastMessage messages.
1511
+ /// </summary>
1512
+ /// <typeparam name="T">Type of Message to be handled.</typeparam>
1513
+ /// <param name="source">Source object to listen for BroadcastMessages on.</param>
1514
+ /// <param name="messageHandler">Function that actually handles the message.</param>
1515
+ /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1516
+ /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1517
+ /// <returns>The de-registration action.</returns>
1518
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastPostProcessor<T>(
1415
1519
  InstanceId source,
1416
- Action<T> originalHandler,
1417
- Action<T> messageHandler,
1520
+ FastHandler<T> originalHandler,
1521
+ FastHandler<T> messageHandler,
1418
1522
  int priority = 0,
1419
1523
  IMessageBus messageBus = null
1420
1524
  )
1421
1525
  where T : IBroadcastMessage
1422
1526
  {
1423
1527
  messageBus = ResolveMessageBus(messageBus);
1424
- Action messageBusDeregistration = messageBus.RegisterBroadcastPostProcessor<T>(
1425
- source,
1426
- messageHandler: this,
1427
- priority: priority
1428
- );
1528
+ MessageBusRegistration messageBusDeregistration =
1529
+ messageBus.RegisterBroadcastPostProcessor<T>(
1530
+ source,
1531
+ priority: priority,
1532
+ messageHandler: this
1533
+ );
1429
1534
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1430
1535
  return typedHandler.AddBroadcastPostProcessor(
1431
1536
  source,
@@ -1438,34 +1543,33 @@ namespace DxMessaging.Core
1438
1543
  }
1439
1544
 
1440
1545
  /// <summary>
1441
- /// Registers this MessageHandler to post processes fast BroadcastMessage messages.
1546
+ /// Registers a sourced-broadcast post-processor whose diagnostics-augmented
1547
+ /// by-ref flat invoker was already built by the caller (the registration
1548
+ /// token), so the default slot stores a single closure instead of an
1549
+ /// <see cref="Action{T}"/> wrapper plus a separately allocated FastHandler
1550
+ /// adapter. <paramref name="originalHandler"/> stays the dedup/identity key.
1442
1551
  /// </summary>
1443
- /// <typeparam name="T">Type of Message to be handled.</typeparam>
1444
- /// <param name="source">Source object to listen for BroadcastMessages on.</param>
1445
- /// <param name="messageHandler">Function that actually handles the message.</param>
1446
- /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1447
- /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1448
- /// <returns>The de-registration action.</returns>
1449
- public Action RegisterSourcedBroadcastPostProcessor<T>(
1552
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastPostProcessor<T>(
1450
1553
  InstanceId source,
1451
- FastHandler<T> originalHandler,
1452
- FastHandler<T> messageHandler,
1554
+ Action<T> originalHandler,
1555
+ FastHandler<T> flatInvoker,
1453
1556
  int priority = 0,
1454
1557
  IMessageBus messageBus = null
1455
1558
  )
1456
1559
  where T : IBroadcastMessage
1457
1560
  {
1458
1561
  messageBus = ResolveMessageBus(messageBus);
1459
- Action messageBusDeregistration = messageBus.RegisterBroadcastPostProcessor<T>(
1460
- source,
1461
- priority: priority,
1462
- messageHandler: this
1463
- );
1562
+ MessageBusRegistration messageBusDeregistration =
1563
+ messageBus.RegisterBroadcastPostProcessor<T>(
1564
+ source,
1565
+ priority: priority,
1566
+ messageHandler: this
1567
+ );
1464
1568
  TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1465
1569
  return typedHandler.AddBroadcastPostProcessor(
1466
1570
  source,
1467
1571
  originalHandler,
1468
- messageHandler,
1572
+ flatInvoker,
1469
1573
  messageBusDeregistration,
1470
1574
  priority,
1471
1575
  messageBus
@@ -1480,7 +1584,7 @@ namespace DxMessaging.Core
1480
1584
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1481
1585
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1482
1586
  /// <returns>The de-registration action.</returns>
1483
- public Action RegisterSourcedBroadcastWithoutSourcePostProcessor<T>(
1587
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastWithoutSourcePostProcessor<T>(
1484
1588
  Action<InstanceId, T> originalHandler,
1485
1589
  Action<InstanceId, T> messageHandler,
1486
1590
  int priority = 0,
@@ -1489,7 +1593,7 @@ namespace DxMessaging.Core
1489
1593
  where T : IBroadcastMessage
1490
1594
  {
1491
1595
  messageBus = ResolveMessageBus(messageBus);
1492
- Action messageBusDeregistration =
1596
+ MessageBusRegistration messageBusDeregistration =
1493
1597
  messageBus.RegisterBroadcastWithoutSourcePostProcessor<T>(
1494
1598
  priority: priority,
1495
1599
  messageHandler: this
@@ -1512,7 +1616,7 @@ namespace DxMessaging.Core
1512
1616
  /// <param name="priority">Priority at which to run the handler, lower runs earlier than higher.</param>
1513
1617
  /// <param name="messageBus">IMessageBus override to register with, if any. Null/not provided defaults to the GlobalMessageBus.</param>
1514
1618
  /// <returns>The de-registration action.</returns>
1515
- public Action RegisterSourcedBroadcastWithoutSourcePostProcessor<T>(
1619
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastWithoutSourcePostProcessor<T>(
1516
1620
  FastHandlerWithContext<T> originalHandler,
1517
1621
  FastHandlerWithContext<T> messageHandler,
1518
1622
  int priority = 0,
@@ -1521,7 +1625,7 @@ namespace DxMessaging.Core
1521
1625
  where T : IBroadcastMessage
1522
1626
  {
1523
1627
  messageBus = ResolveMessageBus(messageBus);
1524
- Action messageBusDeregistration =
1628
+ MessageBusRegistration messageBusDeregistration =
1525
1629
  messageBus.RegisterBroadcastWithoutSourcePostProcessor<T>(
1526
1630
  priority: priority,
1527
1631
  messageHandler: this
@@ -1536,6 +1640,38 @@ namespace DxMessaging.Core
1536
1640
  );
1537
1641
  }
1538
1642
 
1643
+ /// <summary>
1644
+ /// Registers a sourced-broadcast (without-source) post-processor whose
1645
+ /// diagnostics-augmented by-ref context flat invoker was already built by the
1646
+ /// caller (the registration token), so the default slot stores a single closure
1647
+ /// instead of an <see cref="Action{T1, T2}"/> wrapper plus a separately
1648
+ /// allocated FastHandlerWithContext adapter. <paramref name="originalHandler"/>
1649
+ /// stays the dedup/identity key.
1650
+ /// </summary>
1651
+ internal TypedHandler<T>.TypedHandlerDeregistrationState RegisterSourcedBroadcastWithoutSourcePostProcessor<T>(
1652
+ Action<InstanceId, T> originalHandler,
1653
+ FastHandlerWithContext<T> flatInvoker,
1654
+ int priority = 0,
1655
+ IMessageBus messageBus = null
1656
+ )
1657
+ where T : IBroadcastMessage
1658
+ {
1659
+ messageBus = ResolveMessageBus(messageBus);
1660
+ MessageBusRegistration messageBusDeregistration =
1661
+ messageBus.RegisterBroadcastWithoutSourcePostProcessor<T>(
1662
+ priority: priority,
1663
+ messageHandler: this
1664
+ );
1665
+ TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
1666
+ return typedHandler.AddBroadcastWithoutSourcePostProcessor(
1667
+ originalHandler,
1668
+ flatInvoker,
1669
+ messageBusDeregistration,
1670
+ priority,
1671
+ messageBus
1672
+ );
1673
+ }
1674
+
1539
1675
  /// <summary>
1540
1676
  /// Registers an UntargetedInterceptor for messages of the provided type at the provided priority.
1541
1677
  /// </summary>
@@ -1544,7 +1680,7 @@ namespace DxMessaging.Core
1544
1680
  /// <param name="priority">Priority to register the interceptor at (interceptors are run from low -> high priority)</param>
1545
1681
  /// <param name="messageBus">Message bus to register the interceptor on.</param>
1546
1682
  /// <returns>The de-registration action.</returns>
1547
- public Action RegisterUntargetedInterceptor<T>(
1683
+ internal InterceptorDeregistrationState<T> RegisterUntargetedInterceptor<T>(
1548
1684
  IMessageBus.UntargetedInterceptor<T> interceptor,
1549
1685
  int priority = 0,
1550
1686
  IMessageBus messageBus = null
@@ -1552,7 +1688,11 @@ namespace DxMessaging.Core
1552
1688
  where T : IUntargetedMessage
1553
1689
  {
1554
1690
  messageBus = ResolveMessageBus(messageBus);
1555
- return messageBus.RegisterUntargetedInterceptor(interceptor, priority);
1691
+ MessageBusRegistration registration = messageBus.RegisterUntargetedInterceptor(
1692
+ interceptor,
1693
+ priority
1694
+ );
1695
+ return new InterceptorDeregistrationState<T>(messageBus, in registration);
1556
1696
  }
1557
1697
 
1558
1698
  /// <summary>
@@ -1563,7 +1703,7 @@ namespace DxMessaging.Core
1563
1703
  /// <param name="priority">Priority to register the interceptor at (interceptors are run from low -> high priority)</param>
1564
1704
  /// <param name="messageBus">Message bus to register the interceptor on.</param>
1565
1705
  /// <returns>The de-registration action.</returns>
1566
- public Action RegisterBroadcastInterceptor<T>(
1706
+ internal InterceptorDeregistrationState<T> RegisterBroadcastInterceptor<T>(
1567
1707
  IMessageBus.BroadcastInterceptor<T> interceptor,
1568
1708
  int priority = 0,
1569
1709
  IMessageBus messageBus = null
@@ -1571,7 +1711,11 @@ namespace DxMessaging.Core
1571
1711
  where T : IBroadcastMessage
1572
1712
  {
1573
1713
  messageBus = ResolveMessageBus(messageBus);
1574
- return messageBus.RegisterBroadcastInterceptor(interceptor, priority);
1714
+ MessageBusRegistration registration = messageBus.RegisterBroadcastInterceptor(
1715
+ interceptor,
1716
+ priority
1717
+ );
1718
+ return new InterceptorDeregistrationState<T>(messageBus, in registration);
1575
1719
  }
1576
1720
 
1577
1721
  /// <summary>
@@ -1582,7 +1726,7 @@ namespace DxMessaging.Core
1582
1726
  /// <param name="priority">Priority to register the interceptor at (interceptors are run from low -> high priority)</param>
1583
1727
  /// <param name="messageBus">Message bus to register the interceptor on.</param>
1584
1728
  /// <returns>The de-registration action.</returns>
1585
- public Action RegisterTargetedInterceptor<T>(
1729
+ internal InterceptorDeregistrationState<T> RegisterTargetedInterceptor<T>(
1586
1730
  IMessageBus.TargetedInterceptor<T> interceptor,
1587
1731
  int priority = 0,
1588
1732
  IMessageBus messageBus = null
@@ -1590,7 +1734,11 @@ namespace DxMessaging.Core
1590
1734
  where T : ITargetedMessage
1591
1735
  {
1592
1736
  messageBus = ResolveMessageBus(messageBus);
1593
- return messageBus.RegisterTargetedInterceptor(interceptor, priority);
1737
+ MessageBusRegistration registration = messageBus.RegisterTargetedInterceptor(
1738
+ interceptor,
1739
+ priority
1740
+ );
1741
+ return new InterceptorDeregistrationState<T>(messageBus, in registration);
1594
1742
  }
1595
1743
 
1596
1744
  /// <summary>
@@ -1725,6 +1873,94 @@ namespace DxMessaging.Core
1725
1873
  return false;
1726
1874
  }
1727
1875
 
1876
+ internal bool TryObserveFastPriorityHandlerStorage<T>(
1877
+ IMessageBus messageBus,
1878
+ int slotIndex,
1879
+ int priority,
1880
+ out HandlerCacheStorageObservation observation
1881
+ )
1882
+ where T : IMessage
1883
+ {
1884
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1885
+ {
1886
+ observation = default;
1887
+ return false;
1888
+ }
1889
+
1890
+ TypedSlot<T> slot = typedHandler._slots[slotIndex];
1891
+ if (
1892
+ slot == null
1893
+ || !slot.byPriority.TryGetValue(priority, out IHandlerActionCache erased)
1894
+ || erased is not HandlerActionCache<FastHandler<T>> cache
1895
+ )
1896
+ {
1897
+ observation = default;
1898
+ return false;
1899
+ }
1900
+
1901
+ observation = new HandlerCacheStorageObservation(
1902
+ slot.byPriority.Count,
1903
+ 0,
1904
+ slot.byPriority.EnsureCapacity(0),
1905
+ slot.orderedPriorities.Capacity,
1906
+ true,
1907
+ cache.entries.Count,
1908
+ cache.entries.InlineCapacity,
1909
+ cache.entries.MapCapacity,
1910
+ cache.entries.OrderCapacity,
1911
+ cache.entries.UsesSpillStorage
1912
+ );
1913
+ return true;
1914
+ }
1915
+
1916
+ internal readonly struct HandlerCacheStorageObservation
1917
+ {
1918
+ internal HandlerCacheStorageObservation(
1919
+ int priorityEntries,
1920
+ int priorityInlineCapacity,
1921
+ int priorityMapCapacity,
1922
+ int priorityOrderCapacity,
1923
+ bool priorityUsesSpillStorage,
1924
+ int handlerEntries,
1925
+ int handlerInlineCapacity,
1926
+ int handlerMapCapacity,
1927
+ int handlerOrderCapacity,
1928
+ bool handlerUsesSpillStorage
1929
+ )
1930
+ {
1931
+ PriorityEntries = priorityEntries;
1932
+ PriorityInlineCapacity = priorityInlineCapacity;
1933
+ PriorityMapCapacity = priorityMapCapacity;
1934
+ PriorityOrderCapacity = priorityOrderCapacity;
1935
+ PriorityUsesSpillStorage = priorityUsesSpillStorage;
1936
+ HandlerEntries = handlerEntries;
1937
+ HandlerInlineCapacity = handlerInlineCapacity;
1938
+ HandlerMapCapacity = handlerMapCapacity;
1939
+ HandlerOrderCapacity = handlerOrderCapacity;
1940
+ HandlerUsesSpillStorage = handlerUsesSpillStorage;
1941
+ }
1942
+
1943
+ internal int PriorityEntries { get; }
1944
+
1945
+ internal int PriorityInlineCapacity { get; }
1946
+
1947
+ internal int PriorityMapCapacity { get; }
1948
+
1949
+ internal int PriorityOrderCapacity { get; }
1950
+
1951
+ internal bool PriorityUsesSpillStorage { get; }
1952
+
1953
+ internal int HandlerEntries { get; }
1954
+
1955
+ internal int HandlerInlineCapacity { get; }
1956
+
1957
+ internal int HandlerMapCapacity { get; }
1958
+
1959
+ internal int HandlerOrderCapacity { get; }
1960
+
1961
+ internal bool HandlerUsesSpillStorage { get; }
1962
+ }
1963
+
1728
1964
  /// <summary>
1729
1965
  /// Resets empty typed-handler slots associated with
1730
1966
  /// <paramref name="messageBus"/>. The eviction layer calls through
@@ -2058,7 +2294,7 @@ namespace DxMessaging.Core
2058
2294
  && erased is HandlerActionCache<TDelegate> cache
2059
2295
  )
2060
2296
  {
2061
- return cache.insertionOrder.Count;
2297
+ return cache.entries.Count;
2062
2298
  }
2063
2299
 
2064
2300
  return 0;
@@ -2081,19 +2317,11 @@ namespace DxMessaging.Core
2081
2317
  return writeIndex;
2082
2318
  }
2083
2319
 
2084
- List<FastHandler<T>> ordered = cache.insertionOrder;
2085
- int orderedCount = ordered.Count;
2320
+ int orderedCount = cache.entries.Count;
2086
2321
  for (int i = 0; i < orderedCount; ++i)
2087
2322
  {
2088
- if (
2089
- cache.entries.TryGetValue(
2090
- ordered[i],
2091
- out HandlerActionCache<FastHandler<T>>.Entry entry
2092
- )
2093
- )
2094
- {
2095
- target[writeIndex++] = new FlatDispatchEntry<T>(this, entry.handler);
2096
- }
2323
+ HandlerActionCache<FastHandler<T>>.Entry entry = cache.entries.ValueAt(i);
2324
+ target[writeIndex++] = new FlatDispatchEntry<T>(this, entry.handler);
2097
2325
  }
2098
2326
 
2099
2327
  return writeIndex;
@@ -2116,19 +2344,10 @@ namespace DxMessaging.Core
2116
2344
  return writeIndex;
2117
2345
  }
2118
2346
 
2119
- List<Action<T>> ordered = cache.insertionOrder;
2120
- int orderedCount = ordered.Count;
2347
+ int orderedCount = cache.entries.Count;
2121
2348
  for (int i = 0; i < orderedCount; ++i)
2122
2349
  {
2123
- if (
2124
- !cache.entries.TryGetValue(
2125
- ordered[i],
2126
- out HandlerActionCache<Action<T>>.Entry entry
2127
- )
2128
- )
2129
- {
2130
- continue;
2131
- }
2350
+ HandlerActionCache<Action<T>>.Entry entry = cache.entries.ValueAt(i);
2132
2351
 
2133
2352
  // Every default registration path for the flattened slots
2134
2353
  // supplies the adapter at registration time (AddUntargetedHandler,
@@ -2136,6 +2355,12 @@ namespace DxMessaging.Core
2136
2355
  // post-processor siblings). The type test doubles as a null
2137
2356
  // guard; a missing adapter would indicate a new registration
2138
2357
  // path that forgot to provide one.
2358
+ //
2359
+ // INVARIANT: default-slot dispatch consumes entry.flatInvoker.
2360
+ // entry.handler is not a safe dispatch target because
2361
+ // diagnostics-folding Add* overloads may store the raw user
2362
+ // Action there as the identity key. The legacy Handle*/RunHandlers
2363
+ // path uses GetOrAddNewFlatInvokerStack for the same reason.
2139
2364
  if (entry.flatInvoker is FastHandler<T> invoker)
2140
2365
  {
2141
2366
  target[writeIndex++] = new FlatDispatchEntry<T>(this, invoker);
@@ -2171,80 +2396,342 @@ namespace DxMessaging.Core
2171
2396
  return writeIndex;
2172
2397
  }
2173
2398
 
2174
- List<FastHandlerWithContext<T>> ordered = cache.insertionOrder;
2175
- int orderedCount = ordered.Count;
2176
- for (int i = 0; i < orderedCount; ++i)
2177
- {
2178
- if (
2179
- cache.entries.TryGetValue(
2180
- ordered[i],
2181
- out HandlerActionCache<FastHandlerWithContext<T>>.Entry entry
2182
- )
2183
- )
2399
+ int orderedCount = cache.entries.Count;
2400
+ for (int i = 0; i < orderedCount; ++i)
2401
+ {
2402
+ HandlerActionCache<FastHandlerWithContext<T>>.Entry entry = cache.entries.ValueAt(
2403
+ i
2404
+ );
2405
+ target[writeIndex++] = new ContextFlatDispatchEntry<T>(this, entry.handler);
2406
+ }
2407
+
2408
+ return writeIndex;
2409
+ }
2410
+
2411
+ private int FillDefaultWithContextFlatEntries<T>(
2412
+ Dictionary<int, IHandlerActionCache> byPriority,
2413
+ int priority,
2414
+ ContextFlatDispatchEntry<T>[] target,
2415
+ int writeIndex
2416
+ )
2417
+ where T : IMessage
2418
+ {
2419
+ if (
2420
+ byPriority == null
2421
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2422
+ || erased is not HandlerActionCache<Action<InstanceId, T>> cache
2423
+ )
2424
+ {
2425
+ return writeIndex;
2426
+ }
2427
+
2428
+ int orderedCount = cache.entries.Count;
2429
+ for (int i = 0; i < orderedCount; ++i)
2430
+ {
2431
+ HandlerActionCache<Action<InstanceId, T>>.Entry entry = cache.entries.ValueAt(i);
2432
+
2433
+ // See FillDefaultFlatEntries: the adapter is created once at
2434
+ // registration time (AddTargetedWithoutTargetingHandler,
2435
+ // AddSourcedBroadcastWithoutSourceHandler, and their
2436
+ // post-processor siblings).
2437
+ if (entry.flatInvoker is FastHandlerWithContext<T> invoker)
2438
+ {
2439
+ target[writeIndex++] = new ContextFlatDispatchEntry<T>(this, invoker);
2440
+ }
2441
+ else
2442
+ {
2443
+ System.Diagnostics.Debug.Assert(
2444
+ false,
2445
+ "Default with-context registration is missing its "
2446
+ + "FastHandlerWithContext flat invoker; every without-context "
2447
+ + "Add* default path must adapt the augmented handler at "
2448
+ + "registration time."
2449
+ );
2450
+ }
2451
+ }
2452
+
2453
+ return writeIndex;
2454
+ }
2455
+
2456
+ internal sealed class HandlerActionCache<T> : DxMessaging.Core.Internal.IHandlerActionCache
2457
+ {
2458
+ internal struct OrderedEntries
2459
+ {
2460
+ private const int PhysicalInlineCapacity = 2;
2461
+ private static readonly EqualityComparer<T> s_comparer =
2462
+ EqualityComparer<T>.Default;
2463
+
2464
+ private int _inlineCount;
2465
+ private T _key0;
2466
+ private T _key1;
2467
+ private Entry _value0;
2468
+ private Entry _value1;
2469
+ private Dictionary<T, Entry> _spillMap;
2470
+ private List<T> _spillOrder;
2471
+
2472
+ public int Count
2473
+ {
2474
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2475
+ get => _spillMap?.Count ?? _inlineCount;
2476
+ }
2477
+
2478
+ public int InlineCapacity
2479
+ {
2480
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2481
+ get => PhysicalInlineCapacity;
2482
+ }
2483
+
2484
+ public int MapCapacity
2485
+ {
2486
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2487
+ get => _spillMap?.EnsureCapacity(0) ?? 0;
2488
+ }
2489
+
2490
+ public int OrderCapacity
2491
+ {
2492
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2493
+ get => _spillOrder?.Capacity ?? 0;
2494
+ }
2495
+
2496
+ public bool UsesSpillStorage
2497
+ {
2498
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2499
+ get => _spillMap != null;
2500
+ }
2501
+
2502
+ public Entry this[T key]
2503
+ {
2504
+ set
2505
+ {
2506
+ ThrowIfNull(key);
2507
+ if (_spillMap != null)
2508
+ {
2509
+ if (!_spillMap.ContainsKey(key))
2510
+ {
2511
+ _spillMap.Add(key, value);
2512
+ try
2513
+ {
2514
+ _spillOrder.Add(key);
2515
+ }
2516
+ catch
2517
+ {
2518
+ _ = _spillMap.Remove(key);
2519
+ throw;
2520
+ }
2521
+ return;
2522
+ }
2523
+ _spillMap[key] = value;
2524
+ return;
2525
+ }
2526
+
2527
+ int index = FindInlineIndex(key);
2528
+ if (index >= 0)
2529
+ {
2530
+ SetInlineValue(index, value);
2531
+ return;
2532
+ }
2533
+ if (_inlineCount < PhysicalInlineCapacity)
2534
+ {
2535
+ SetInline(_inlineCount, key, value);
2536
+ ++_inlineCount;
2537
+ return;
2538
+ }
2539
+
2540
+ Spill();
2541
+ _spillMap.Add(key, value);
2542
+ try
2543
+ {
2544
+ _spillOrder.Add(key);
2545
+ }
2546
+ catch
2547
+ {
2548
+ _ = _spillMap.Remove(key);
2549
+ throw;
2550
+ }
2551
+ }
2552
+ }
2553
+
2554
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2555
+ public bool ContainsKey(T key)
2556
+ {
2557
+ ThrowIfNull(key);
2558
+ return _spillMap?.ContainsKey(key) ?? FindInlineIndex(key) >= 0;
2559
+ }
2560
+
2561
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2562
+ public bool TryGetValue(T key, out Entry value)
2563
+ {
2564
+ ThrowIfNull(key);
2565
+ if (_spillMap != null)
2566
+ {
2567
+ return _spillMap.TryGetValue(key, out value);
2568
+ }
2569
+ int index = FindInlineIndex(key);
2570
+ if (index >= 0)
2571
+ {
2572
+ value = GetInlineValue(index);
2573
+ return true;
2574
+ }
2575
+ value = default;
2576
+ return false;
2577
+ }
2578
+
2579
+ public bool Remove(T key)
2580
+ {
2581
+ ThrowIfNull(key);
2582
+ if (_spillMap != null)
2583
+ {
2584
+ if (!_spillMap.Remove(key))
2585
+ {
2586
+ return false;
2587
+ }
2588
+ _ = _spillOrder.Remove(key);
2589
+ return true;
2590
+ }
2591
+ int index = FindInlineIndex(key);
2592
+ if (index < 0)
2593
+ {
2594
+ return false;
2595
+ }
2596
+ int lastIndex = _inlineCount - 1;
2597
+ for (int moveIndex = index; moveIndex < lastIndex; ++moveIndex)
2598
+ {
2599
+ SetInline(
2600
+ moveIndex,
2601
+ GetInlineKey(moveIndex + 1),
2602
+ GetInlineValue(moveIndex + 1)
2603
+ );
2604
+ }
2605
+ ClearInline(lastIndex);
2606
+ --_inlineCount;
2607
+ return true;
2608
+ }
2609
+
2610
+ public void Clear()
2611
+ {
2612
+ if (_spillMap != null)
2613
+ {
2614
+ _spillMap.Clear();
2615
+ _spillOrder.Clear();
2616
+ return;
2617
+ }
2618
+ for (int index = 0; index < _inlineCount; ++index)
2619
+ {
2620
+ ClearInline(index);
2621
+ }
2622
+ _inlineCount = 0;
2623
+ }
2624
+
2625
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2626
+ public T KeyAt(int index)
2627
+ {
2628
+ if (_spillOrder != null)
2629
+ {
2630
+ return _spillOrder[index];
2631
+ }
2632
+ if ((uint)index >= (uint)_inlineCount)
2633
+ {
2634
+ throw new ArgumentOutOfRangeException(nameof(index));
2635
+ }
2636
+ return GetInlineKey(index);
2637
+ }
2638
+
2639
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2640
+ public Entry ValueAt(int index)
2641
+ {
2642
+ if (_spillOrder != null)
2643
+ {
2644
+ return _spillMap[_spillOrder[index]];
2645
+ }
2646
+ if ((uint)index >= (uint)_inlineCount)
2647
+ {
2648
+ throw new ArgumentOutOfRangeException(nameof(index));
2649
+ }
2650
+ return GetInlineValue(index);
2651
+ }
2652
+
2653
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2654
+ private int FindInlineIndex(T key)
2655
+ {
2656
+ if (_inlineCount > 0 && s_comparer.Equals(_key0, key))
2657
+ {
2658
+ return 0;
2659
+ }
2660
+ if (_inlineCount > 1 && s_comparer.Equals(_key1, key))
2661
+ {
2662
+ return 1;
2663
+ }
2664
+ return -1;
2665
+ }
2666
+
2667
+ private void Spill()
2184
2668
  {
2185
- target[writeIndex++] = new ContextFlatDispatchEntry<T>(this, entry.handler);
2669
+ Dictionary<T, Entry> map = new(PhysicalInlineCapacity * 2);
2670
+ List<T> order = new(PhysicalInlineCapacity * 2);
2671
+ for (int index = 0; index < _inlineCount; ++index)
2672
+ {
2673
+ T key = GetInlineKey(index);
2674
+ map.Add(key, GetInlineValue(index));
2675
+ order.Add(key);
2676
+ }
2677
+ for (int index = 0; index < _inlineCount; ++index)
2678
+ {
2679
+ ClearInline(index);
2680
+ }
2681
+ _inlineCount = 0;
2682
+ _spillMap = map;
2683
+ _spillOrder = order;
2186
2684
  }
2187
- }
2188
2685
 
2189
- return writeIndex;
2190
- }
2686
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2687
+ private T GetInlineKey(int index)
2688
+ {
2689
+ return index == 0 ? _key0 : _key1;
2690
+ }
2191
2691
 
2192
- private int FillDefaultWithContextFlatEntries<T>(
2193
- Dictionary<int, IHandlerActionCache> byPriority,
2194
- int priority,
2195
- ContextFlatDispatchEntry<T>[] target,
2196
- int writeIndex
2197
- )
2198
- where T : IMessage
2199
- {
2200
- if (
2201
- byPriority == null
2202
- || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2203
- || erased is not HandlerActionCache<Action<InstanceId, T>> cache
2204
- )
2205
- {
2206
- return writeIndex;
2207
- }
2692
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2693
+ private Entry GetInlineValue(int index)
2694
+ {
2695
+ return index == 0 ? _value0 : _value1;
2696
+ }
2208
2697
 
2209
- List<Action<InstanceId, T>> ordered = cache.insertionOrder;
2210
- int orderedCount = ordered.Count;
2211
- for (int i = 0; i < orderedCount; ++i)
2212
- {
2213
- if (
2214
- !cache.entries.TryGetValue(
2215
- ordered[i],
2216
- out HandlerActionCache<Action<InstanceId, T>>.Entry entry
2217
- )
2218
- )
2698
+ private void SetInline(int index, T key, Entry value)
2219
2699
  {
2220
- continue;
2700
+ if (index == 0)
2701
+ {
2702
+ _key0 = key;
2703
+ _value0 = value;
2704
+ return;
2705
+ }
2706
+ _key1 = key;
2707
+ _value1 = value;
2221
2708
  }
2222
2709
 
2223
- // See FillDefaultFlatEntries: the adapter is created once at
2224
- // registration time (AddTargetedWithoutTargetingHandler,
2225
- // AddSourcedBroadcastWithoutSourceHandler, and their
2226
- // post-processor siblings).
2227
- if (entry.flatInvoker is FastHandlerWithContext<T> invoker)
2710
+ private void SetInlineValue(int index, Entry value)
2228
2711
  {
2229
- target[writeIndex++] = new ContextFlatDispatchEntry<T>(this, invoker);
2712
+ if (index == 0)
2713
+ {
2714
+ _value0 = value;
2715
+ return;
2716
+ }
2717
+ _value1 = value;
2230
2718
  }
2231
- else
2719
+
2720
+ private void ClearInline(int index)
2232
2721
  {
2233
- System.Diagnostics.Debug.Assert(
2234
- false,
2235
- "Default with-context registration is missing its "
2236
- + "FastHandlerWithContext flat invoker; every without-context "
2237
- + "Add* default path must adapt the augmented handler at "
2238
- + "registration time."
2239
- );
2722
+ SetInline(index, default, default);
2240
2723
  }
2241
- }
2242
2724
 
2243
- return writeIndex;
2244
- }
2725
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2726
+ private static void ThrowIfNull(T key)
2727
+ {
2728
+ if (key is null)
2729
+ {
2730
+ throw new ArgumentNullException(nameof(key));
2731
+ }
2732
+ }
2733
+ }
2245
2734
 
2246
- internal sealed class HandlerActionCache<T> : DxMessaging.Core.Internal.IHandlerActionCache
2247
- {
2248
2735
  // Uses outer T as a field type -- reflection callers must close
2249
2736
  // via MakeGenericType(outer.GetGenericArguments()) before passing
2250
2737
  // this type to Activator.CreateInstance. See
@@ -2273,40 +2760,48 @@ namespace DxMessaging.Core
2273
2760
  this.flatInvoker = flatInvoker;
2274
2761
  }
2275
2762
 
2763
+ // The stored handler delegate. For default Action<TMessage>
2764
+ // slots this is the dedup/refcount delegate and is not the
2765
+ // dispatch target; both bus-side flat dispatch and legacy
2766
+ // Handle*/RunHandlers dispatch go through `flatInvoker`.
2767
+ // Registration paths that pre-build the augmented invoker store
2768
+ // the RAW user handler here (so no extra augmented Action wrapper
2769
+ // is allocated); paths that adapt at registration time store the
2770
+ // augmented handler. For fast/global slots, by contrast,
2771
+ // `handler` IS the dispatched delegate.
2276
2772
  public readonly T handler;
2277
2773
  public readonly int count;
2278
2774
 
2279
- // Pre-resolved invoker consumed by the bus-side flat dispatch
2280
- // snapshot (MessageBus.BuildMessageFlatDispatch). For
2281
- // default Action<TMessage> registrations this holds a
2282
- // FastHandler<TMessage> adapter wrapping the AUGMENTED
2283
- // handler, created exactly ONCE at registration time so
2284
- // snapshot rebuilds never allocate closures. For delegate
2285
- // shapes the flat path does not consume (fast handlers, which
2286
- // already ARE the invoker, and every targeted/broadcast
2287
- // shape) this stays null. Refcount increments and decrements
2775
+ // Pre-resolved invoker consumed by bus-side flat dispatch
2776
+ // snapshots and legacy default-slot Handle*/RunHandlers
2777
+ // snapshots. For default Action<TMessage> registrations this holds the
2778
+ // diagnostics-AUGMENTED FastHandler<TMessage> closure (either a
2779
+ // standalone adapter wrapping an augmented Action, or the single
2780
+ // folded augmented closure the registration token now builds
2781
+ // directly), created exactly ONCE at registration time so
2782
+ // snapshot rebuilds never allocate closures. It -- not
2783
+ // `handler` -- is the dispatch target for default slots.
2784
+ // For delegate shapes the flat path does not consume (fast
2785
+ // handlers, which already ARE the invoker, and global accept-all
2786
+ // shapes) this stays null. Refcount increments and decrements
2288
2787
  // preserve the first registration's invoker, mirroring the
2289
- // first-augmented-handler-wins semantics of `handler`.
2788
+ // first-registration-wins semantics of `handler`.
2290
2789
  public readonly object flatInvoker;
2291
2790
  }
2292
2791
 
2293
- public readonly Dictionary<T, Entry> entries = new();
2294
-
2295
- // Original-handler keys in first-registration order. Dictionary
2296
- // enumeration order is NOT stable across Remove/Add churn (.NET
2297
- // reuses freed slots LIFO), so dispatch snapshots are rebuilt from
2298
- // this list instead of from <see cref="entries"/> to honor the
2299
- // documented "same priority uses registration order" contract.
2300
- // Invariants: contains exactly the keys of <see cref="entries"/>;
2301
- // a key is appended on its FIRST registration only (refcount
2302
- // increments do not move it) and removed when its refcount drops
2303
- // to zero. Maintained exclusively by the AddHandler* family and
2304
- // <see cref="DxMessaging.Core.Internal.IHandlerActionCache.Reset"/>.
2305
- public readonly List<T> insertionOrder = new();
2792
+ // The first two distinct handlers live directly in this object.
2793
+ // The third materializes a dictionary plus an ordered key list;
2794
+ // that spill remains allocated for reuse after removal or reset.
2795
+ // Both representations preserve first-registration order and make
2796
+ // entry/order drift structurally impossible.
2797
+ public OrderedEntries entries;
2306
2798
  public readonly List<T> cache = new();
2799
+ private System.Collections.IList _flatInvokerCache;
2307
2800
  public long version;
2308
2801
  public long lastSeenVersion = -1;
2309
2802
  public long lastSeenEmissionId = -1;
2803
+ public long flatInvokerLastSeenVersion = -1;
2804
+ public long flatInvokerLastSeenEmissionId = -1;
2310
2805
 
2311
2806
  /// <summary>Monotonic version field, read-only on the interface surface.</summary>
2312
2807
  long DxMessaging.Core.Internal.IHandlerActionCache.Version
@@ -2333,7 +2828,7 @@ namespace DxMessaging.Core
2333
2828
  set => lastSeenEmissionId = value;
2334
2829
  }
2335
2830
 
2336
- /// <summary>True iff the entries dictionary holds zero handlers.</summary>
2831
+ /// <summary>True iff the ordered entry storage holds zero handlers.</summary>
2337
2832
  bool DxMessaging.Core.Internal.IHandlerActionCache.IsEmpty
2338
2833
  {
2339
2834
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2347,15 +2842,75 @@ namespace DxMessaging.Core
2347
2842
  void DxMessaging.Core.Internal.IHandlerActionCache.Reset()
2348
2843
  {
2349
2844
  entries.Clear();
2350
- insertionOrder.Clear();
2351
2845
  cache.Clear();
2846
+ _flatInvokerCache?.Clear();
2352
2847
  lastSeenVersion = -1;
2353
2848
  lastSeenEmissionId = -1;
2849
+ flatInvokerLastSeenVersion = -1;
2850
+ flatInvokerLastSeenEmissionId = -1;
2354
2851
  unchecked
2355
2852
  {
2356
2853
  ++version;
2357
2854
  }
2358
2855
  }
2856
+
2857
+ /// <summary>
2858
+ /// Non-generic membership probe; casts the boxed key to the
2859
+ /// cache's delegate shape. Mirrors
2860
+ /// <c>entries.ContainsKey(originalHandler)</c>.
2861
+ /// </summary>
2862
+ bool DxMessaging.Core.Internal.IHandlerActionCache.ContainsEntry(object originalHandler)
2863
+ {
2864
+ return entries.ContainsKey((T)originalHandler);
2865
+ }
2866
+
2867
+ /// <summary>Bumps <see cref="version"/> by one (the closure's top-of-body <c>version++</c>).</summary>
2868
+ void DxMessaging.Core.Internal.IHandlerActionCache.BumpVersion()
2869
+ {
2870
+ version++;
2871
+ }
2872
+
2873
+ /// <summary>
2874
+ /// Non-generic decrement-or-remove mirroring the legacy
2875
+ /// deregistration closure's refcount logic exactly.
2876
+ /// </summary>
2877
+ bool DxMessaging.Core.Internal.IHandlerActionCache.DeregisterEntry(
2878
+ object originalHandler,
2879
+ out bool wasLastForEntry
2880
+ )
2881
+ {
2882
+ wasLastForEntry = false;
2883
+ T key = (T)originalHandler;
2884
+ if (!entries.TryGetValue(key, out Entry entry))
2885
+ {
2886
+ return false;
2887
+ }
2888
+
2889
+ if (entry.count <= 1)
2890
+ {
2891
+ _ = entries.Remove(key);
2892
+ version++;
2893
+ wasLastForEntry = true;
2894
+ return true;
2895
+ }
2896
+
2897
+ entries[key] = new Entry(entry.handler, entry.count - 1, entry.flatInvoker);
2898
+ return true;
2899
+ }
2900
+
2901
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2902
+ internal List<TInvoker> GetOrCreateFlatInvokerCache<TInvoker>()
2903
+ where TInvoker : class
2904
+ {
2905
+ if (_flatInvokerCache == null)
2906
+ {
2907
+ List<TInvoker> typedCache = new();
2908
+ _flatInvokerCache = typedCache;
2909
+ return typedCache;
2910
+ }
2911
+
2912
+ return (List<TInvoker>)_flatInvokerCache;
2913
+ }
2359
2914
  }
2360
2915
 
2361
2916
  /// <summary>
@@ -2386,6 +2941,197 @@ namespace DxMessaging.Core
2386
2941
 
2387
2942
  internal bool _markedForOuterRemoval;
2388
2943
 
2944
+ /// <summary>
2945
+ /// Reusable teardown state for the typed-handler registration path,
2946
+ /// replacing the deregistration <see cref="Action"/> closure
2947
+ /// the two instance <c>AddHandlerPreservingPriorityKey</c> overloads
2948
+ /// used to build and return. All of the closure's captured locals
2949
+ /// are stored as fields; <see cref="Deregister"/> re-expresses the
2950
+ /// closure body verbatim, choosing the scalar or context cache
2951
+ /// resolution by <see cref="_keyed"/> and using the non-generic
2952
+ /// <see cref="IHandlerActionCache"/> teardown ops
2953
+ /// (<c>ContainsEntry</c> / <c>BumpVersion</c> / <c>DeregisterEntry</c>)
2954
+ /// so it needs no delegate-shape type argument. Tokens embed this value
2955
+ /// in their existing Registration object; compatibility callers can
2956
+ /// convert it to an allocation-backed wrapper or <see cref="Action"/>.
2957
+ /// </summary>
2958
+ internal readonly struct TypedHandlerDeregistrationState
2959
+ {
2960
+ // Captured state (was the closure's captures).
2961
+ private readonly IMessageBus _messageBus;
2962
+ private readonly long _resetGeneration;
2963
+ private readonly TypedSlot<T> _slot;
2964
+ private readonly long _slotVersion;
2965
+
2966
+ // Scalar path: priority -> cache. Null on the context path.
2967
+ private readonly Dictionary<int, IHandlerActionCache> _handlers;
2968
+
2969
+ // Context path: context -> (priority -> cache). Null on the scalar path.
2970
+ private readonly Dictionary<
2971
+ InstanceId,
2972
+ Dictionary<int, IHandlerActionCache>
2973
+ > _handlersByContext;
2974
+ private readonly InstanceId _context;
2975
+
2976
+ private readonly bool _keyed;
2977
+ private readonly object _originalHandler;
2978
+ private readonly int _priority;
2979
+ private readonly MessageBusRegistration _busRegistration;
2980
+
2981
+ // Scalar constructor.
2982
+ internal TypedHandlerDeregistrationState(
2983
+ IMessageBus messageBus,
2984
+ long resetGeneration,
2985
+ TypedSlot<T> slot,
2986
+ long slotVersion,
2987
+ Dictionary<int, IHandlerActionCache> handlers,
2988
+ object originalHandler,
2989
+ int priority,
2990
+ in MessageBusRegistration busRegistration
2991
+ )
2992
+ {
2993
+ _messageBus = messageBus;
2994
+ _resetGeneration = resetGeneration;
2995
+ _slot = slot;
2996
+ _slotVersion = slotVersion;
2997
+ _handlers = handlers;
2998
+ _handlersByContext = null;
2999
+ _context = default;
3000
+ _keyed = false;
3001
+ _originalHandler = originalHandler;
3002
+ _priority = priority;
3003
+ _busRegistration = busRegistration;
3004
+ }
3005
+
3006
+ // Context constructor.
3007
+ internal TypedHandlerDeregistrationState(
3008
+ IMessageBus messageBus,
3009
+ long resetGeneration,
3010
+ TypedSlot<T> slot,
3011
+ long slotVersion,
3012
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext,
3013
+ InstanceId context,
3014
+ object originalHandler,
3015
+ int priority,
3016
+ in MessageBusRegistration busRegistration
3017
+ )
3018
+ {
3019
+ _messageBus = messageBus;
3020
+ _resetGeneration = resetGeneration;
3021
+ _slot = slot;
3022
+ _slotVersion = slotVersion;
3023
+ _handlers = null;
3024
+ _handlersByContext = handlersByContext;
3025
+ _context = context;
3026
+ _keyed = true;
3027
+ _originalHandler = originalHandler;
3028
+ _priority = priority;
3029
+ _busRegistration = busRegistration;
3030
+ }
3031
+
3032
+ internal void Deregister()
3033
+ {
3034
+ if (
3035
+ !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
3036
+ _messageBus,
3037
+ _resetGeneration
3038
+ )
3039
+ )
3040
+ {
3041
+ return;
3042
+ }
3043
+
3044
+ if (_slot != null && _slot.version != _slotVersion)
3045
+ {
3046
+ return;
3047
+ }
3048
+
3049
+ IHandlerActionCache cache;
3050
+ if (_keyed)
3051
+ {
3052
+ if (
3053
+ !_handlersByContext.TryGetValue(
3054
+ _context,
3055
+ out Dictionary<int, IHandlerActionCache> sortedHandlers
3056
+ )
3057
+ )
3058
+ {
3059
+ return;
3060
+ }
3061
+
3062
+ if (!sortedHandlers.TryGetValue(_priority, out cache) || cache == null)
3063
+ {
3064
+ return;
3065
+ }
3066
+ }
3067
+ else
3068
+ {
3069
+ if (!_handlers.TryGetValue(_priority, out cache) || cache == null)
3070
+ {
3071
+ return;
3072
+ }
3073
+ }
3074
+
3075
+ if (!cache.ContainsEntry(_originalHandler))
3076
+ {
3077
+ return;
3078
+ }
3079
+
3080
+ cache.BumpVersion();
3081
+
3082
+ _messageBus.Deregister<T>(in _busRegistration);
3083
+ if (_slot != null)
3084
+ {
3085
+ _slot.lastTouchTicks =
3086
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
3087
+ _messageBus
3088
+ );
3089
+ }
3090
+
3091
+ _ = cache.DeregisterEntry(_originalHandler, out bool wasLastForEntry);
3092
+ if (wasLastForEntry && _slot != null)
3093
+ {
3094
+ _slot.liveCount--;
3095
+ }
3096
+ // Deliberately keep the priority and context mappings to
3097
+ // preserve frozen snapshots for the current emission.
3098
+ }
3099
+
3100
+ public static implicit operator HandlerDeregistration(
3101
+ TypedHandlerDeregistrationState state
3102
+ )
3103
+ {
3104
+ return new TypedHandlerDeregistration(state);
3105
+ }
3106
+
3107
+ public static implicit operator Action(TypedHandlerDeregistrationState state)
3108
+ {
3109
+ TypedHandlerDeregistration deregistration = new(state);
3110
+ return deregistration.Deregister;
3111
+ }
3112
+ }
3113
+
3114
+ /// <summary>
3115
+ /// Allocation-backed compatibility/spill wrapper. Token registrations keep
3116
+ /// <see cref="TypedHandlerDeregistrationState"/> inline on their existing
3117
+ /// registration object; direct handler callers and overlapping retry state
3118
+ /// use this wrapper only when independent object identity is required.
3119
+ /// </summary>
3120
+ internal sealed class TypedHandlerDeregistration : HandlerDeregistration
3121
+ {
3122
+ private readonly TypedHandlerDeregistrationState _state;
3123
+
3124
+ internal TypedHandlerDeregistration(TypedHandlerDeregistrationState state)
3125
+ {
3126
+ _state = state;
3127
+ }
3128
+
3129
+ internal override void Deregister()
3130
+ {
3131
+ _state.Deregister();
3132
+ }
3133
+ }
3134
+
2389
3135
  int ITypedHandlerSlotSweeper.MessageTypeIndex
2390
3136
  {
2391
3137
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -3137,11 +3883,11 @@ namespace DxMessaging.Core
3137
3883
  /// <param name="deregistration">Deregistration action for the handler.</param>
3138
3884
  /// <param name="priority">Priority at which to add the handler.</param>
3139
3885
  /// <returns>De-registration action to unregister the handler.</returns>
3140
- public Action AddTargetedHandler(
3886
+ public TypedHandlerDeregistrationState AddTargetedHandler(
3141
3887
  InstanceId target,
3142
3888
  Action<T> originalHandler,
3143
3889
  Action<T> handler,
3144
- Action deregistration,
3890
+ MessageBusRegistration deregistration,
3145
3891
  int priority,
3146
3892
  IMessageBus messageBus
3147
3893
  )
@@ -3162,6 +3908,35 @@ namespace DxMessaging.Core
3162
3908
  );
3163
3909
  }
3164
3910
 
3911
+ /// <summary>
3912
+ /// Adds a TargetedHandler whose by-ref flat invoker was already built
3913
+ /// by the caller, so the default slot stores a single closure instead
3914
+ /// of an <see cref="Action{T}"/> wrapper plus a separately allocated
3915
+ /// FastHandler adapter (the registration token folds diagnostics into
3916
+ /// one <see cref="FastHandler{T}"/>). <paramref name="originalHandler"/>
3917
+ /// is the dedup/identity key and is never invoked for the default slot.
3918
+ /// </summary>
3919
+ internal TypedHandlerDeregistrationState AddTargetedHandler(
3920
+ InstanceId target,
3921
+ Action<T> originalHandler,
3922
+ FastHandler<T> flatInvoker,
3923
+ MessageBusRegistration deregistration,
3924
+ int priority,
3925
+ IMessageBus messageBus
3926
+ )
3927
+ {
3928
+ return AddHandlerPreservingPriorityKey(
3929
+ target,
3930
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedHandleDefault),
3931
+ originalHandler,
3932
+ originalHandler,
3933
+ deregistration,
3934
+ priority,
3935
+ messageBus,
3936
+ flatInvoker
3937
+ );
3938
+ }
3939
+
3165
3940
  /// <summary>
3166
3941
  /// Adds a fast TargetedHandler to listen to Messages of the given type, returning a deregistration action.
3167
3942
  /// </summary>
@@ -3170,11 +3945,11 @@ namespace DxMessaging.Core
3170
3945
  /// <param name="deregistration">Deregistration action for the handler.</param>
3171
3946
  /// <param name="priority">Priority at which to add the handler.</param>
3172
3947
  /// <returns>De-registration action to unregister the handler.</returns>
3173
- public Action AddTargetedHandler(
3948
+ public TypedHandlerDeregistrationState AddTargetedHandler(
3174
3949
  InstanceId target,
3175
3950
  FastHandler<T> originalHandler,
3176
3951
  FastHandler<T> handler,
3177
- Action deregistration,
3952
+ MessageBusRegistration deregistration,
3178
3953
  int priority,
3179
3954
  IMessageBus messageBus
3180
3955
  )
@@ -3197,10 +3972,10 @@ namespace DxMessaging.Core
3197
3972
  /// <param name="deregistration">Deregistration action for the handler.</param>
3198
3973
  /// <param name="priority">Priority at which to add the handler.</param>
3199
3974
  /// <returns>De-registration action to unregister the handler.</returns>
3200
- public Action AddTargetedWithoutTargetingHandler(
3975
+ public TypedHandlerDeregistrationState AddTargetedWithoutTargetingHandler(
3201
3976
  Action<InstanceId, T> originalHandler,
3202
3977
  Action<InstanceId, T> handler,
3203
- Action deregistration,
3978
+ MessageBusRegistration deregistration,
3204
3979
  int priority,
3205
3980
  IMessageBus messageBus
3206
3981
  )
@@ -3225,6 +4000,36 @@ namespace DxMessaging.Core
3225
4000
  );
3226
4001
  }
3227
4002
 
4003
+ /// <summary>
4004
+ /// Adds a TargetedWithoutTargetingHandler whose by-ref-with-context flat
4005
+ /// invoker was already built by the caller, so the default slot stores a
4006
+ /// single closure instead of an <see cref="Action{InstanceId, T}"/>
4007
+ /// wrapper plus a separately allocated FastHandlerWithContext adapter.
4008
+ /// <paramref name="originalHandler"/> is the dedup/identity key and is
4009
+ /// never invoked for the default slot.
4010
+ /// </summary>
4011
+ internal TypedHandlerDeregistrationState AddTargetedWithoutTargetingHandler(
4012
+ Action<InstanceId, T> originalHandler,
4013
+ FastHandlerWithContext<T> flatInvoker,
4014
+ MessageBusRegistration deregistration,
4015
+ int priority,
4016
+ IMessageBus messageBus
4017
+ )
4018
+ {
4019
+ return AddHandlerPreservingPriorityKey(
4020
+ GetOrCreatePriorityHandlers(
4021
+ TypedSlotIndex.TargetedHandleWithoutContext,
4022
+ requiresContext: false
4023
+ ),
4024
+ originalHandler,
4025
+ originalHandler,
4026
+ deregistration,
4027
+ priority,
4028
+ messageBus,
4029
+ flatInvoker
4030
+ );
4031
+ }
4032
+
3228
4033
  /// <summary>
3229
4034
  /// Adds a fast TargetedWithoutTargetingHandler to listen to Messages of the given type, returning a deregistration action.
3230
4035
  /// </summary>
@@ -3232,10 +4037,10 @@ namespace DxMessaging.Core
3232
4037
  /// <param name="deregistration">Deregistration action for the handler.</param>
3233
4038
  /// <param name="priority">Priority at which to add the handler.</param>
3234
4039
  /// <returns>De-registration action to unregister the handler.</returns>
3235
- public Action AddTargetedWithoutTargetingHandler(
4040
+ public TypedHandlerDeregistrationState AddTargetedWithoutTargetingHandler(
3236
4041
  FastHandlerWithContext<T> originalHandler,
3237
4042
  FastHandlerWithContext<T> handler,
3238
- Action deregistration,
4043
+ MessageBusRegistration deregistration,
3239
4044
  int priority,
3240
4045
  IMessageBus messageBus
3241
4046
  )
@@ -3254,31 +4059,63 @@ namespace DxMessaging.Core
3254
4059
  }
3255
4060
 
3256
4061
  /// <summary>
3257
- /// Adds a UntargetedHandler to listen to Messages of the given type, returning a deregistration action.
4062
+ /// Adds a UntargetedHandler to listen to Messages of the given type, returning a deregistration action.
4063
+ /// </summary>
4064
+ /// <param name="handler">Relevant MessageHandler.</param>
4065
+ /// <param name="deregistration">Deregistration action for the handler.</param>
4066
+ /// <param name="priority">Priority at which to add the handler.</param>
4067
+ /// <returns>De-registration action to unregister the handler.</returns>
4068
+ public TypedHandlerDeregistrationState AddUntargetedHandler(
4069
+ Action<T> originalHandler,
4070
+ Action<T> handler,
4071
+ MessageBusRegistration deregistration,
4072
+ int priority,
4073
+ IMessageBus messageBus
4074
+ )
4075
+ {
4076
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
4077
+ // at registration time, so bus-side flat snapshot rebuilds
4078
+ // resolve default registrations without allocating closures.
4079
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
4080
+ return AddHandlerPreservingPriorityKey(
4081
+ GetOrCreatePriorityHandlers(
4082
+ TypedSlotIndex.UntargetedHandleDefault,
4083
+ requiresContext: false
4084
+ ),
4085
+ originalHandler,
4086
+ handler,
4087
+ deregistration,
4088
+ priority,
4089
+ messageBus,
4090
+ flatInvoker
4091
+ );
4092
+ }
4093
+
4094
+ /// <summary>
4095
+ /// Adds an UntargetedHandler whose by-ref flat invoker was already
4096
+ /// built by the caller, so the default slot stores a single closure
4097
+ /// instead of an <see cref="Action{T}"/> wrapper plus a separately
4098
+ /// allocated FastHandler adapter. The registration token uses this to
4099
+ /// fold diagnostics into one <see cref="FastHandler{T}"/>.
4100
+ /// <paramref name="originalHandler"/> is the dedup/identity key; it is
4101
+ /// stored as the entry handler but is never invoked for the default
4102
+ /// slot, which dispatches through <paramref name="flatInvoker"/>.
3258
4103
  /// </summary>
3259
- /// <param name="handler">Relevant MessageHandler.</param>
3260
- /// <param name="deregistration">Deregistration action for the handler.</param>
3261
- /// <param name="priority">Priority at which to add the handler.</param>
3262
- /// <returns>De-registration action to unregister the handler.</returns>
3263
- public Action AddUntargetedHandler(
4104
+ internal TypedHandlerDeregistrationState AddUntargetedHandler(
3264
4105
  Action<T> originalHandler,
3265
- Action<T> handler,
3266
- Action deregistration,
4106
+ FastHandler<T> flatInvoker,
4107
+ MessageBusRegistration deregistration,
3267
4108
  int priority,
3268
4109
  IMessageBus messageBus
3269
4110
  )
3270
4111
  {
3271
- // Adapt the AUGMENTED handler to FastHandler form exactly once,
3272
- // at registration time, so bus-side flat snapshot rebuilds
3273
- // resolve default registrations without allocating closures.
3274
- FastHandler<T> flatInvoker = (ref T message) => handler(message);
3275
4112
  return AddHandlerPreservingPriorityKey(
3276
4113
  GetOrCreatePriorityHandlers(
3277
4114
  TypedSlotIndex.UntargetedHandleDefault,
3278
4115
  requiresContext: false
3279
4116
  ),
3280
4117
  originalHandler,
3281
- handler,
4118
+ originalHandler,
3282
4119
  deregistration,
3283
4120
  priority,
3284
4121
  messageBus,
@@ -3293,10 +4130,10 @@ namespace DxMessaging.Core
3293
4130
  /// <param name="deregistration">Deregistration action for the handler.</param>
3294
4131
  /// <param name="priority">Priority at which to add the handler.</param>
3295
4132
  /// <returns>De-registration action to unregister the handler.</returns>
3296
- public Action AddUntargetedHandler(
4133
+ public TypedHandlerDeregistrationState AddUntargetedHandler(
3297
4134
  FastHandler<T> originalHandler,
3298
4135
  FastHandler<T> handler,
3299
- Action deregistration,
4136
+ MessageBusRegistration deregistration,
3300
4137
  int priority,
3301
4138
  IMessageBus messageBus
3302
4139
  )
@@ -3322,11 +4159,11 @@ namespace DxMessaging.Core
3322
4159
  /// <param name="deregistration">Deregistration action for the handler.</param>
3323
4160
  /// <param name="priority">Priority at which to add the handler.</param>
3324
4161
  /// <returns>De-registration action to unregister the handler.</returns>
3325
- public Action AddSourcedBroadcastHandler(
4162
+ public TypedHandlerDeregistrationState AddSourcedBroadcastHandler(
3326
4163
  InstanceId source,
3327
4164
  Action<T> originalHandler,
3328
4165
  Action<T> handler,
3329
- Action deregistration,
4166
+ MessageBusRegistration deregistration,
3330
4167
  int priority,
3331
4168
  IMessageBus messageBus
3332
4169
  )
@@ -3347,6 +4184,34 @@ namespace DxMessaging.Core
3347
4184
  );
3348
4185
  }
3349
4186
 
4187
+ /// <summary>
4188
+ /// Adds a SourcedBroadcastHandler whose by-ref flat invoker was already
4189
+ /// built by the caller, so the default slot stores a single closure
4190
+ /// instead of an <see cref="Action{T}"/> wrapper plus a separately
4191
+ /// allocated FastHandler adapter. <paramref name="originalHandler"/> is
4192
+ /// the dedup/identity key and is never invoked for the default slot.
4193
+ /// </summary>
4194
+ internal TypedHandlerDeregistrationState AddSourcedBroadcastHandler(
4195
+ InstanceId source,
4196
+ Action<T> originalHandler,
4197
+ FastHandler<T> flatInvoker,
4198
+ MessageBusRegistration deregistration,
4199
+ int priority,
4200
+ IMessageBus messageBus
4201
+ )
4202
+ {
4203
+ return AddHandlerPreservingPriorityKey(
4204
+ source,
4205
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastHandleDefault),
4206
+ originalHandler,
4207
+ originalHandler,
4208
+ deregistration,
4209
+ priority,
4210
+ messageBus,
4211
+ flatInvoker
4212
+ );
4213
+ }
4214
+
3350
4215
  /// <summary>
3351
4216
  /// Adds a fast SourcedBroadcastHandler to listen to Messages of the given type from an entity, returning a deregistration action.
3352
4217
  /// </summary>
@@ -3355,11 +4220,11 @@ namespace DxMessaging.Core
3355
4220
  /// <param name="deregistration">Deregistration action for the handler.</param>
3356
4221
  /// <param name="priority">Priority at which to add the handler.</param>
3357
4222
  /// <returns>De-registration action to unregister the handler.</returns>
3358
- public Action AddSourcedBroadcastHandler(
4223
+ public TypedHandlerDeregistrationState AddSourcedBroadcastHandler(
3359
4224
  InstanceId source,
3360
4225
  FastHandler<T> originalHandler,
3361
4226
  FastHandler<T> handler,
3362
- Action deregistration,
4227
+ MessageBusRegistration deregistration,
3363
4228
  int priority,
3364
4229
  IMessageBus messageBus
3365
4230
  )
@@ -3382,10 +4247,10 @@ namespace DxMessaging.Core
3382
4247
  /// <param name="deregistration">Deregistration action for the handler.</param>
3383
4248
  /// <param name="priority">Priority at which to add the handler.</param>
3384
4249
  /// <returns>De-registration action to unregister the handler.</returns>
3385
- public Action AddSourcedBroadcastWithoutSourceHandler(
4250
+ public TypedHandlerDeregistrationState AddSourcedBroadcastWithoutSourceHandler(
3386
4251
  Action<InstanceId, T> originalHandler,
3387
4252
  Action<InstanceId, T> handler,
3388
- Action deregistration,
4253
+ MessageBusRegistration deregistration,
3389
4254
  int priority,
3390
4255
  IMessageBus messageBus
3391
4256
  )
@@ -3411,6 +4276,37 @@ namespace DxMessaging.Core
3411
4276
  );
3412
4277
  }
3413
4278
 
4279
+ /// <summary>
4280
+ /// Adds a SourcedBroadcastWithoutSourceHandler whose by-ref-with-context
4281
+ /// flat invoker was already built by the caller, so the default slot
4282
+ /// stores a single closure instead of an
4283
+ /// <see cref="Action{InstanceId, T}"/> wrapper plus a separately
4284
+ /// allocated FastHandlerWithContext adapter.
4285
+ /// <paramref name="originalHandler"/> is the dedup/identity key and is
4286
+ /// never invoked for the default slot.
4287
+ /// </summary>
4288
+ internal TypedHandlerDeregistrationState AddSourcedBroadcastWithoutSourceHandler(
4289
+ Action<InstanceId, T> originalHandler,
4290
+ FastHandlerWithContext<T> flatInvoker,
4291
+ MessageBusRegistration deregistration,
4292
+ int priority,
4293
+ IMessageBus messageBus
4294
+ )
4295
+ {
4296
+ return AddHandlerPreservingPriorityKey(
4297
+ GetOrCreatePriorityHandlers(
4298
+ TypedSlotIndex.BroadcastHandleWithoutContext,
4299
+ requiresContext: false
4300
+ ),
4301
+ originalHandler,
4302
+ originalHandler,
4303
+ deregistration,
4304
+ priority,
4305
+ messageBus,
4306
+ flatInvoker
4307
+ );
4308
+ }
4309
+
3414
4310
  /// <summary>
3415
4311
  /// Adds a fast SourcedBroadcastWithoutSourceHandler to listen to Messages of the given type from an entity, returning a deregistration action.
3416
4312
  /// </summary>
@@ -3418,10 +4314,10 @@ namespace DxMessaging.Core
3418
4314
  /// <param name="deregistration">Deregistration action for the handler.</param>
3419
4315
  /// <param name="priority">Priority at which to add the handler.</param>
3420
4316
  /// <returns>De-registration action to unregister the handler.</returns>
3421
- public Action AddSourcedBroadcastWithoutSourceHandler(
4317
+ public TypedHandlerDeregistrationState AddSourcedBroadcastWithoutSourceHandler(
3422
4318
  FastHandlerWithContext<T> originalHandler,
3423
4319
  FastHandlerWithContext<T> handler,
3424
- Action deregistration,
4320
+ MessageBusRegistration deregistration,
3425
4321
  int priority,
3426
4322
  IMessageBus messageBus
3427
4323
  )
@@ -3444,12 +4340,10 @@ namespace DxMessaging.Core
3444
4340
  /// Adds a Global UntargetedHandler to listen to all Untargeted Messages of all types, returning the deregistration action.
3445
4341
  /// </summary>
3446
4342
  /// <param name="handler">Relevant MessageHandler.</param>
3447
- /// <param name="deregistration">Deregistration action for the handler.</param>
3448
4343
  /// <returns>De-registration action to unregister the handler.</returns>
3449
- public Action AddGlobalUntargetedHandler(
4344
+ public GlobalHandlerDeregistrationState AddGlobalUntargetedHandler(
3450
4345
  Action<IUntargetedMessage> originalHandler,
3451
4346
  Action<IUntargetedMessage> handler,
3452
- Action deregistration,
3453
4347
  IMessageBus messageBus
3454
4348
  )
3455
4349
  {
@@ -3457,7 +4351,6 @@ namespace DxMessaging.Core
3457
4351
  GetOrCreateGlobalSlot(TypedGlobalSlotIndex.UntargetedDefault),
3458
4352
  originalHandler,
3459
4353
  handler,
3460
- deregistration,
3461
4354
  messageBus
3462
4355
  );
3463
4356
  }
@@ -3466,12 +4359,10 @@ namespace DxMessaging.Core
3466
4359
  /// Adds a Global fast UntargetedHandler to listen to all Untargeted Messages of all types, returning the deregistration action.
3467
4360
  /// </summary>
3468
4361
  /// <param name="handler">Relevant MessageHandler.</param>
3469
- /// <param name="deregistration">Deregistration action for the handler.</param>
3470
4362
  /// <returns>De-registration action to unregister the handler.</returns>
3471
- public Action AddGlobalUntargetedHandler(
4363
+ public GlobalHandlerDeregistrationState AddGlobalUntargetedHandler(
3472
4364
  FastHandler<IUntargetedMessage> originalHandler,
3473
4365
  FastHandler<IUntargetedMessage> handler,
3474
- Action deregistration,
3475
4366
  IMessageBus messageBus
3476
4367
  )
3477
4368
  {
@@ -3479,7 +4370,6 @@ namespace DxMessaging.Core
3479
4370
  GetOrCreateGlobalSlot(TypedGlobalSlotIndex.UntargetedFast),
3480
4371
  originalHandler,
3481
4372
  handler,
3482
- deregistration,
3483
4373
  messageBus
3484
4374
  );
3485
4375
  }
@@ -3488,12 +4378,10 @@ namespace DxMessaging.Core
3488
4378
  /// Adds a Global TargetedHandler to listen to all Targeted Messages of all types for all entities, returning the deregistration action.
3489
4379
  /// </summary>
3490
4380
  /// <param name="handler">Relevant MessageHandler.</param>
3491
- /// <param name="deregistration">Deregistration action for the handler.</param>
3492
4381
  /// <returns>De-registration action to unregister the handler.</returns>
3493
- public Action AddGlobalTargetedHandler(
4382
+ public GlobalHandlerDeregistrationState AddGlobalTargetedHandler(
3494
4383
  Action<InstanceId, ITargetedMessage> originalHandler,
3495
4384
  Action<InstanceId, ITargetedMessage> handler,
3496
- Action deregistration,
3497
4385
  IMessageBus messageBus
3498
4386
  )
3499
4387
  {
@@ -3501,7 +4389,6 @@ namespace DxMessaging.Core
3501
4389
  GetOrCreateGlobalSlot(TypedGlobalSlotIndex.TargetedDefault),
3502
4390
  originalHandler,
3503
4391
  handler,
3504
- deregistration,
3505
4392
  messageBus
3506
4393
  );
3507
4394
  }
@@ -3510,12 +4397,10 @@ namespace DxMessaging.Core
3510
4397
  /// Adds a Global fast TargetedHandler to listen to all Targeted Messages of all types for all entities (along with the target instance id), returning the deregistration action.
3511
4398
  /// </summary>
3512
4399
  /// <param name="handler">Relevant MessageHandler.</param>
3513
- /// <param name="deregistration">Deregistration action for the handler.</param>
3514
4400
  /// <returns>De-registration action to unregister the handler.</returns>
3515
- public Action AddGlobalTargetedHandler(
4401
+ public GlobalHandlerDeregistrationState AddGlobalTargetedHandler(
3516
4402
  FastHandlerWithContext<ITargetedMessage> originalHandler,
3517
4403
  FastHandlerWithContext<ITargetedMessage> handler,
3518
- Action deregistration,
3519
4404
  IMessageBus messageBus
3520
4405
  )
3521
4406
  {
@@ -3523,7 +4408,6 @@ namespace DxMessaging.Core
3523
4408
  GetOrCreateGlobalSlot(TypedGlobalSlotIndex.TargetedFast),
3524
4409
  originalHandler,
3525
4410
  handler,
3526
- deregistration,
3527
4411
  messageBus
3528
4412
  );
3529
4413
  }
@@ -3532,12 +4416,10 @@ namespace DxMessaging.Core
3532
4416
  /// Adds a Global BroadcastHandler to listen to all Targeted Messages of all types for all entities, returning the deregistration action.
3533
4417
  /// </summary>
3534
4418
  /// <param name="handler">Relevant MessageHandler.</param>
3535
- /// <param name="deregistration">Deregistration action for the handler.</param>
3536
4419
  /// <returns>De-registration action to unregister the handler.</returns>
3537
- public Action AddGlobalBroadcastHandler(
4420
+ public GlobalHandlerDeregistrationState AddGlobalBroadcastHandler(
3538
4421
  Action<InstanceId, IBroadcastMessage> originalHandler,
3539
4422
  Action<InstanceId, IBroadcastMessage> handler,
3540
- Action deregistration,
3541
4423
  IMessageBus messageBus
3542
4424
  )
3543
4425
  {
@@ -3545,7 +4427,6 @@ namespace DxMessaging.Core
3545
4427
  GetOrCreateGlobalSlot(TypedGlobalSlotIndex.BroadcastDefault),
3546
4428
  originalHandler,
3547
4429
  handler,
3548
- deregistration,
3549
4430
  messageBus
3550
4431
  );
3551
4432
  }
@@ -3554,12 +4435,10 @@ namespace DxMessaging.Core
3554
4435
  /// Adds a Global fast BroadcastHandler to listen to all Targeted Messages of all types for all entities (along with the source instance id), returning the deregistration action.
3555
4436
  /// </summary>
3556
4437
  /// <param name="handler">Relevant MessageHandler.</param>
3557
- /// <param name="deregistration">Deregistration action for the handler.</param>
3558
4438
  /// <returns>De-registration action to unregister the handler.</returns>
3559
- public Action AddGlobalBroadcastHandler(
4439
+ public GlobalHandlerDeregistrationState AddGlobalBroadcastHandler(
3560
4440
  FastHandlerWithContext<IBroadcastMessage> originalHandler,
3561
4441
  FastHandlerWithContext<IBroadcastMessage> handler,
3562
- Action deregistration,
3563
4442
  IMessageBus messageBus
3564
4443
  )
3565
4444
  {
@@ -3567,7 +4446,6 @@ namespace DxMessaging.Core
3567
4446
  GetOrCreateGlobalSlot(TypedGlobalSlotIndex.BroadcastFast),
3568
4447
  originalHandler,
3569
4448
  handler,
3570
- deregistration,
3571
4449
  messageBus
3572
4450
  );
3573
4451
  }
@@ -3579,10 +4457,10 @@ namespace DxMessaging.Core
3579
4457
  /// <param name="deregistration">Deregistration action for the handler.</param>
3580
4458
  /// <param name="priority">Priority at which to add the handler.</param>
3581
4459
  /// <returns>De-registration action to unregister the handler.</returns>
3582
- public Action AddUntargetedPostProcessor(
4460
+ public TypedHandlerDeregistrationState AddUntargetedPostProcessor(
3583
4461
  Action<T> originalHandler,
3584
4462
  Action<T> handler,
3585
- Action deregistration,
4463
+ MessageBusRegistration deregistration,
3586
4464
  int priority,
3587
4465
  IMessageBus messageBus
3588
4466
  )
@@ -3612,10 +4490,10 @@ namespace DxMessaging.Core
3612
4490
  /// <param name="deregistration">Deregistration action for the handler.</param>
3613
4491
  /// <param name="priority">Priority at which to add the handler.</param>
3614
4492
  /// <returns>De-registration action to unregister the handler.</returns>
3615
- public Action AddUntargetedPostProcessor(
4493
+ public TypedHandlerDeregistrationState AddUntargetedPostProcessor(
3616
4494
  FastHandler<T> originalHandler,
3617
4495
  FastHandler<T> handler,
3618
- Action deregistration,
4496
+ MessageBusRegistration deregistration,
3619
4497
  int priority,
3620
4498
  IMessageBus messageBus
3621
4499
  )
@@ -3641,11 +4519,11 @@ namespace DxMessaging.Core
3641
4519
  /// <param name="deregistration">Deregistration action for the handler.</param>
3642
4520
  /// <param name="priority">Priority at which to add the handler.</param>
3643
4521
  /// <returns>De-registration action to unregister the handler.</returns>
3644
- public Action AddTargetedPostProcessor(
4522
+ public TypedHandlerDeregistrationState AddTargetedPostProcessor(
3645
4523
  InstanceId target,
3646
4524
  Action<T> originalHandler,
3647
4525
  Action<T> handler,
3648
- Action deregistration,
4526
+ MessageBusRegistration deregistration,
3649
4527
  int priority,
3650
4528
  IMessageBus messageBus
3651
4529
  )
@@ -3666,6 +4544,35 @@ namespace DxMessaging.Core
3666
4544
  );
3667
4545
  }
3668
4546
 
4547
+ /// <summary>
4548
+ /// Adds a Targeted post-processor whose by-ref flat invoker was already
4549
+ /// built by the caller (the registration token folds diagnostics into one
4550
+ /// <see cref="FastHandler{T}"/>), so the default slot stores a single
4551
+ /// closure instead of an <see cref="Action{T}"/> wrapper plus a separately
4552
+ /// allocated FastHandler adapter. <paramref name="originalHandler"/> is the
4553
+ /// dedup/identity key and is never invoked for the default slot.
4554
+ /// </summary>
4555
+ internal TypedHandlerDeregistrationState AddTargetedPostProcessor(
4556
+ InstanceId target,
4557
+ Action<T> originalHandler,
4558
+ FastHandler<T> flatInvoker,
4559
+ MessageBusRegistration deregistration,
4560
+ int priority,
4561
+ IMessageBus messageBus
4562
+ )
4563
+ {
4564
+ return AddHandlerPreservingPriorityKey(
4565
+ target,
4566
+ GetOrCreateContextHandlers(TypedSlotIndex.TargetedPostProcessDefault),
4567
+ originalHandler,
4568
+ originalHandler,
4569
+ deregistration,
4570
+ priority,
4571
+ messageBus,
4572
+ flatInvoker
4573
+ );
4574
+ }
4575
+
3669
4576
  /// <summary>
3670
4577
  /// Adds a Targeted post-processor to be called after all other handlers have been called.
3671
4578
  /// </summary>
@@ -3674,11 +4581,11 @@ namespace DxMessaging.Core
3674
4581
  /// <param name="deregistration">Deregistration action for the handler.</param>
3675
4582
  /// <param name="priority">Priority at which to add the handler.</param>
3676
4583
  /// <returns>De-registration action to unregister the handler.</returns>
3677
- public Action AddTargetedPostProcessor(
4584
+ public TypedHandlerDeregistrationState AddTargetedPostProcessor(
3678
4585
  InstanceId target,
3679
4586
  FastHandler<T> originalHandler,
3680
4587
  FastHandler<T> handler,
3681
- Action deregistration,
4588
+ MessageBusRegistration deregistration,
3682
4589
  int priority,
3683
4590
  IMessageBus messageBus
3684
4591
  )
@@ -3701,10 +4608,10 @@ namespace DxMessaging.Core
3701
4608
  /// <param name="deregistration">Deregistration action for the handler.</param>
3702
4609
  /// <param name="priority">Priority at which to add the handler.</param>
3703
4610
  /// <returns>De-registration action to unregister the handler.</returns>
3704
- public Action AddTargetedWithoutTargetingPostProcessor(
4611
+ public TypedHandlerDeregistrationState AddTargetedWithoutTargetingPostProcessor(
3705
4612
  Action<InstanceId, T> originalHandler,
3706
4613
  Action<InstanceId, T> handler,
3707
- Action deregistration,
4614
+ MessageBusRegistration deregistration,
3708
4615
  int priority,
3709
4616
  IMessageBus messageBus
3710
4617
  )
@@ -3729,6 +4636,38 @@ namespace DxMessaging.Core
3729
4636
  );
3730
4637
  }
3731
4638
 
4639
+ /// <summary>
4640
+ /// Adds a Targeted (without-targeting) post-processor whose by-ref
4641
+ /// context flat invoker was already built by the caller (the registration
4642
+ /// token folds diagnostics into one
4643
+ /// <see cref="FastHandlerWithContext{T}"/>), so the default slot stores a
4644
+ /// single closure instead of an <see cref="Action{T1, T2}"/> wrapper plus a
4645
+ /// separately allocated FastHandlerWithContext adapter.
4646
+ /// <paramref name="originalHandler"/> is the dedup/identity key and is never
4647
+ /// invoked for the default slot.
4648
+ /// </summary>
4649
+ internal TypedHandlerDeregistrationState AddTargetedWithoutTargetingPostProcessor(
4650
+ Action<InstanceId, T> originalHandler,
4651
+ FastHandlerWithContext<T> flatInvoker,
4652
+ MessageBusRegistration deregistration,
4653
+ int priority,
4654
+ IMessageBus messageBus
4655
+ )
4656
+ {
4657
+ return AddHandlerPreservingPriorityKey(
4658
+ GetOrCreatePriorityHandlers(
4659
+ TypedSlotIndex.TargetedPostProcessWithoutContext,
4660
+ requiresContext: false
4661
+ ),
4662
+ originalHandler,
4663
+ originalHandler,
4664
+ deregistration,
4665
+ priority,
4666
+ messageBus,
4667
+ flatInvoker
4668
+ );
4669
+ }
4670
+
3732
4671
  /// <summary>
3733
4672
  /// Adds a Targeted post-processor to be called after all other handlers have been called after every message of the given type.
3734
4673
  /// </summary>
@@ -3736,10 +4675,10 @@ namespace DxMessaging.Core
3736
4675
  /// <param name="deregistration">Deregistration action for the handler.</param>
3737
4676
  /// <param name="priority">Priority at which to add the handler.</param>
3738
4677
  /// <returns>De-registration action to unregister the handler.</returns>
3739
- public Action AddTargetedWithoutTargetingPostProcessor(
4678
+ public TypedHandlerDeregistrationState AddTargetedWithoutTargetingPostProcessor(
3740
4679
  FastHandlerWithContext<T> originalHandler,
3741
4680
  FastHandlerWithContext<T> handler,
3742
- Action deregistration,
4681
+ MessageBusRegistration deregistration,
3743
4682
  int priority,
3744
4683
  IMessageBus messageBus
3745
4684
  )
@@ -3765,11 +4704,11 @@ namespace DxMessaging.Core
3765
4704
  /// <param name="deregistration">Deregistration action for the handler.</param>
3766
4705
  /// <param name="priority">Priority at which to add the handler.</param>
3767
4706
  /// <returns>De-registration action to unregister the handler.</returns>
3768
- public Action AddBroadcastPostProcessor(
4707
+ public TypedHandlerDeregistrationState AddBroadcastPostProcessor(
3769
4708
  InstanceId source,
3770
4709
  Action<T> originalHandler,
3771
4710
  Action<T> handler,
3772
- Action deregistration,
4711
+ MessageBusRegistration deregistration,
3773
4712
  int priority,
3774
4713
  IMessageBus messageBus
3775
4714
  )
@@ -3790,6 +4729,35 @@ namespace DxMessaging.Core
3790
4729
  );
3791
4730
  }
3792
4731
 
4732
+ /// <summary>
4733
+ /// Adds a Broadcast post-processor whose by-ref flat invoker was already
4734
+ /// built by the caller (the registration token folds diagnostics into one
4735
+ /// <see cref="FastHandler{T}"/>), so the default slot stores a single
4736
+ /// closure instead of an <see cref="Action{T}"/> wrapper plus a separately
4737
+ /// allocated FastHandler adapter. <paramref name="originalHandler"/> is the
4738
+ /// dedup/identity key and is never invoked for the default slot.
4739
+ /// </summary>
4740
+ internal TypedHandlerDeregistrationState AddBroadcastPostProcessor(
4741
+ InstanceId source,
4742
+ Action<T> originalHandler,
4743
+ FastHandler<T> flatInvoker,
4744
+ MessageBusRegistration deregistration,
4745
+ int priority,
4746
+ IMessageBus messageBus
4747
+ )
4748
+ {
4749
+ return AddHandlerPreservingPriorityKey(
4750
+ source,
4751
+ GetOrCreateContextHandlers(TypedSlotIndex.BroadcastPostProcessDefault),
4752
+ originalHandler,
4753
+ originalHandler,
4754
+ deregistration,
4755
+ priority,
4756
+ messageBus,
4757
+ flatInvoker
4758
+ );
4759
+ }
4760
+
3793
4761
  /// <summary>
3794
4762
  /// Adds a fast Broadcast post-processor to be called after all other handlers have been called.
3795
4763
  /// </summary>
@@ -3798,11 +4766,11 @@ namespace DxMessaging.Core
3798
4766
  /// <param name="deregistration">Deregistration action for the handler.</param>
3799
4767
  /// <param name="priority">Priority at which to add the handler.</param>
3800
4768
  /// <returns>De-registration action to unregister the handler.</returns>
3801
- public Action AddBroadcastPostProcessor(
4769
+ public TypedHandlerDeregistrationState AddBroadcastPostProcessor(
3802
4770
  InstanceId source,
3803
4771
  FastHandler<T> originalHandler,
3804
4772
  FastHandler<T> handler,
3805
- Action deregistration,
4773
+ MessageBusRegistration deregistration,
3806
4774
  int priority,
3807
4775
  IMessageBus messageBus
3808
4776
  )
@@ -3825,10 +4793,10 @@ namespace DxMessaging.Core
3825
4793
  /// <param name="deregistration">Deregistration action for the handler.</param>
3826
4794
  /// <param name="priority">Priority at which to add the handler.</param>
3827
4795
  /// <returns>De-registration action to unregister the handler.</returns>
3828
- public Action AddBroadcastWithoutSourcePostProcessor(
4796
+ public TypedHandlerDeregistrationState AddBroadcastWithoutSourcePostProcessor(
3829
4797
  Action<InstanceId, T> originalHandler,
3830
4798
  Action<InstanceId, T> handler,
3831
- Action deregistration,
4799
+ MessageBusRegistration deregistration,
3832
4800
  int priority,
3833
4801
  IMessageBus messageBus
3834
4802
  )
@@ -3853,6 +4821,37 @@ namespace DxMessaging.Core
3853
4821
  );
3854
4822
  }
3855
4823
 
4824
+ /// <summary>
4825
+ /// Adds a Broadcast (without-source) post-processor whose by-ref context
4826
+ /// flat invoker was already built by the caller (the registration token
4827
+ /// folds diagnostics into one <see cref="FastHandlerWithContext{T}"/>), so
4828
+ /// the default slot stores a single closure instead of an
4829
+ /// <see cref="Action{T1, T2}"/> wrapper plus a separately allocated
4830
+ /// FastHandlerWithContext adapter. <paramref name="originalHandler"/> is the
4831
+ /// dedup/identity key and is never invoked for the default slot.
4832
+ /// </summary>
4833
+ internal TypedHandlerDeregistrationState AddBroadcastWithoutSourcePostProcessor(
4834
+ Action<InstanceId, T> originalHandler,
4835
+ FastHandlerWithContext<T> flatInvoker,
4836
+ MessageBusRegistration deregistration,
4837
+ int priority,
4838
+ IMessageBus messageBus
4839
+ )
4840
+ {
4841
+ return AddHandlerPreservingPriorityKey(
4842
+ GetOrCreatePriorityHandlers(
4843
+ TypedSlotIndex.BroadcastPostProcessWithoutContext,
4844
+ requiresContext: false
4845
+ ),
4846
+ originalHandler,
4847
+ originalHandler,
4848
+ deregistration,
4849
+ priority,
4850
+ messageBus,
4851
+ flatInvoker
4852
+ );
4853
+ }
4854
+
3856
4855
  /// <summary>
3857
4856
  /// Adds a fast Broadcast post-processor to be called after all other handlers have been called.
3858
4857
  /// </summary>
@@ -3860,10 +4859,10 @@ namespace DxMessaging.Core
3860
4859
  /// <param name="deregistration">Deregistration action for the handler.</param>
3861
4860
  /// <param name="priority">Priority at which to add the handler.</param>
3862
4861
  /// <returns>De-registration action to unregister the handler.</returns>
3863
- public Action AddBroadcastWithoutSourcePostProcessor(
4862
+ public TypedHandlerDeregistrationState AddBroadcastWithoutSourcePostProcessor(
3864
4863
  FastHandlerWithContext<T> originalHandler,
3865
4864
  FastHandlerWithContext<T> handler,
3866
- Action deregistration,
4865
+ MessageBusRegistration deregistration,
3867
4866
  int priority,
3868
4867
  IMessageBus messageBus
3869
4868
  )
@@ -3900,12 +4899,12 @@ namespace DxMessaging.Core
3900
4899
  // default-shape registrations the bus-side flat snapshot consumes
3901
4900
  // (FastHandler adapter wrapping the augmented handler); see
3902
4901
  // HandlerActionCache.Entry.flatInvoker.
3903
- private Action AddHandlerPreservingPriorityKey<TU>(
4902
+ private TypedHandlerDeregistrationState AddHandlerPreservingPriorityKey<TU>(
3904
4903
  InstanceId context,
3905
4904
  Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext,
3906
4905
  TU originalHandler,
3907
4906
  TU augmentedHandler,
3908
- Action deregistration,
4907
+ MessageBusRegistration deregistration,
3909
4908
  int priority,
3910
4909
  IMessageBus messageBus,
3911
4910
  object flatInvoker = null
@@ -3926,141 +4925,67 @@ namespace DxMessaging.Core
3926
4925
  !sortedHandlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
3927
4926
  || erasedCache is not HandlerActionCache<TU> cache
3928
4927
  )
3929
- {
3930
- cache = new HandlerActionCache<TU>();
3931
- sortedHandlers[priority] = cache;
3932
- }
3933
-
3934
- if (
3935
- !cache.entries.TryGetValue(
3936
- originalHandler,
3937
- out HandlerActionCache<TU>.Entry entry
3938
- )
3939
- )
3940
- {
3941
- entry = new HandlerActionCache<TU>.Entry(augmentedHandler, 0);
3942
- }
3943
-
3944
- bool firstRegistration = entry.count == 0;
3945
- entry = firstRegistration
3946
- ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1, flatInvoker)
3947
- : new HandlerActionCache<TU>.Entry(
3948
- entry.handler,
3949
- entry.count + 1,
3950
- entry.flatInvoker
3951
- );
3952
-
3953
- cache.entries[originalHandler] = entry;
3954
- if (firstRegistration)
3955
- {
3956
- cache.insertionOrder.Add(originalHandler);
3957
- }
3958
- cache.version++;
3959
- TypedSlot<T> slot = FindContextSlot(handlersByContext);
3960
- if (slot != null)
3961
- {
3962
- slot.lastTouchTicks =
3963
- global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
3964
- messageBus
3965
- );
3966
- }
3967
- if (firstRegistration && slot != null)
3968
- {
3969
- slot.liveCount++;
3970
- }
3971
-
3972
- Dictionary<
3973
- InstanceId,
3974
- Dictionary<int, IHandlerActionCache>
3975
- > localHandlersByContext = handlersByContext;
3976
- TypedSlot<T> localSlot = slot;
3977
- long localSlotVersion = slot?.version ?? 0;
3978
- long localResetGeneration =
3979
- global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
3980
-
3981
- return () =>
3982
- {
3983
- if (
3984
- !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
3985
- messageBus,
3986
- localResetGeneration
3987
- )
3988
- )
3989
- {
3990
- return;
3991
- }
3992
-
3993
- if (localSlot != null && localSlot.version != localSlotVersion)
3994
- {
3995
- return;
3996
- }
3997
-
3998
- if (!localHandlersByContext.TryGetValue(context, out sortedHandlers))
3999
- {
4000
- return;
4001
- }
4002
-
4003
- if (
4004
- !sortedHandlers.TryGetValue(
4005
- priority,
4006
- out IHandlerActionCache localErasedCache
4007
- ) || localErasedCache is not HandlerActionCache<TU> localCache
4008
- )
4009
- {
4010
- return;
4011
- }
4012
-
4013
- if (
4014
- !localCache.entries.TryGetValue(
4015
- originalHandler,
4016
- out HandlerActionCache<TU>.Entry localEntry
4017
- )
4018
- )
4019
- {
4020
- return;
4021
- }
4928
+ {
4929
+ cache = new HandlerActionCache<TU>();
4930
+ sortedHandlers[priority] = cache;
4931
+ }
4022
4932
 
4023
- localCache.version++;
4933
+ if (
4934
+ !cache.entries.TryGetValue(
4935
+ originalHandler,
4936
+ out HandlerActionCache<TU>.Entry entry
4937
+ )
4938
+ )
4939
+ {
4940
+ entry = new HandlerActionCache<TU>.Entry(augmentedHandler, 0);
4941
+ }
4024
4942
 
4025
- deregistration?.Invoke();
4026
- if (localSlot != null)
4027
- {
4028
- localSlot.lastTouchTicks =
4029
- global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
4030
- messageBus
4031
- );
4032
- }
4943
+ bool firstRegistration = entry.count == 0;
4944
+ entry = firstRegistration
4945
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1, flatInvoker)
4946
+ : new HandlerActionCache<TU>.Entry(
4947
+ entry.handler,
4948
+ entry.count + 1,
4949
+ entry.flatInvoker
4950
+ );
4033
4951
 
4034
- if (localEntry.count <= 1)
4035
- {
4036
- _ = localCache.entries.Remove(originalHandler);
4037
- // List.Remove is O(n) over the same-priority bucket.
4038
- // Accepted tradeoff (here and at every sibling
4039
- // deregistration site): buckets are small in practice,
4040
- // removal is a cold churn path, and the list keeps
4041
- // steady-state dispatch allocation-free while
4042
- // preserving first-registration order, unlike
4043
- // Dictionary enumeration whose freed slots are reused
4044
- // LIFO.
4045
- _ = localCache.insertionOrder.Remove(originalHandler);
4046
- localCache.version++;
4047
- if (localSlot != null)
4048
- {
4049
- localSlot.liveCount--;
4050
- }
4051
- // Deliberately keep the priority and context mappings to preserve
4052
- // frozen snapshots for the current emission.
4053
- return;
4054
- }
4952
+ cache.entries[originalHandler] = entry;
4953
+ cache.version++;
4954
+ TypedSlot<T> slot = FindContextSlot(handlersByContext);
4955
+ if (slot != null)
4956
+ {
4957
+ slot.lastTouchTicks =
4958
+ global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
4959
+ messageBus
4960
+ );
4961
+ }
4962
+ if (firstRegistration && slot != null)
4963
+ {
4964
+ slot.liveCount++;
4965
+ }
4055
4966
 
4056
- localEntry = new HandlerActionCache<TU>.Entry(
4057
- localEntry.handler,
4058
- localEntry.count - 1,
4059
- localEntry.flatInvoker
4060
- );
4967
+ long localSlotVersion = slot?.version ?? 0;
4968
+ long localResetGeneration =
4969
+ global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
4061
4970
 
4062
- localCache.entries[originalHandler] = localEntry;
4063
- };
4971
+ // The per-handle teardown object re-expresses the old
4972
+ // deregistration closure verbatim (generation + slot-version
4973
+ // guards, cache resolution, version bumps, refcount
4974
+ // decrement-or-remove, liveCount, keep-key-during-emission)
4975
+ // using the non-generic IHandlerActionCache teardown ops, so it
4976
+ // carries no TU type argument and replaces the closure's display
4977
+ // class + delegate with this single object.
4978
+ return new TypedHandlerDeregistrationState(
4979
+ messageBus,
4980
+ localResetGeneration,
4981
+ slot,
4982
+ localSlotVersion,
4983
+ handlersByContext,
4984
+ context,
4985
+ originalHandler,
4986
+ priority,
4987
+ in deregistration
4988
+ );
4064
4989
  }
4065
4990
 
4066
4991
  private static Action AddHandlerPreservingPriorityKey<TU>(
@@ -4112,10 +5037,6 @@ namespace DxMessaging.Core
4112
5037
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
4113
5038
 
4114
5039
  cache.entries[originalHandler] = entry;
4115
- if (firstRegistration)
4116
- {
4117
- cache.insertionOrder.Add(originalHandler);
4118
- }
4119
5040
  cache.version++;
4120
5041
 
4121
5042
  Dictionary<
@@ -4154,7 +5075,6 @@ namespace DxMessaging.Core
4154
5075
  if (localEntry.count <= 1)
4155
5076
  {
4156
5077
  _ = localCache.entries.Remove(originalHandler);
4157
- _ = localCache.insertionOrder.Remove(originalHandler);
4158
5078
  localCache.version++;
4159
5079
  // Deliberately keep the priority and context mappings to preserve
4160
5080
  // frozen snapshots for the current emission.
@@ -4485,7 +5405,7 @@ namespace DxMessaging.Core
4485
5405
  where TMessage : IMessage
4486
5406
  where TU : IMessage
4487
5407
  {
4488
- // Snapshot semantics: do not bail on the live entries dictionary
5408
+ // Snapshot semantics: do not bail on the live entry storage
4489
5409
  // count. A mid-emit removal can drain entries while the pinned
4490
5410
  // emission snapshot in cache.cache still holds the handlers we
4491
5411
  // must invoke. Read the snapshot first and bail only if the
@@ -4493,13 +5413,13 @@ namespace DxMessaging.Core
4493
5413
  //
4494
5414
  // Perf note: GetOrAddNewHandlerStack is now invoked on every
4495
5415
  // call (including for empty caches that the previous fast-path
4496
- // would have skipped). The cost is one dictionary
5416
+ // would have skipped). The cost is one
4497
5417
  // emission-id/version compare and -- only when the per-emission
4498
5418
  // snapshot has not been pinned yet -- a single pass over
4499
5419
  // cache.entries to materialise an empty list. The win is
4500
5420
  // correctness across cross-handler mid-emit removals where the
4501
5421
  // pinned snapshot in cache.cache still holds handlers the live
4502
- // entries dictionary no longer reaches.
5422
+ // entry storage no longer reaches.
4503
5423
  if (cache == null)
4504
5424
  {
4505
5425
  return;
@@ -4608,7 +5528,7 @@ namespace DxMessaging.Core
4608
5528
  {
4609
5529
  // Snapshot semantics: see comment on the FastHandler<TU> overload.
4610
5530
  // The pinned emission snapshot may still hold handlers even when
4611
- // the live entries dictionary has been drained mid-emit.
5531
+ // the live entry storage has been drained mid-emit.
4612
5532
  if (cache == null)
4613
5533
  {
4614
5534
  return;
@@ -4946,91 +5866,94 @@ namespace DxMessaging.Core
4946
5866
  return;
4947
5867
  }
4948
5868
 
4949
- List<Action<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
5869
+ List<FastHandler<T>> handlers = GetOrAddNewFlatInvokerStack<
5870
+ Action<T>,
5871
+ FastHandler<T>
5872
+ >(cache, emissionId);
4950
5873
  ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
4951
5874
  int handlersCount = handlers.Count;
4952
5875
  switch (handlersCount)
4953
5876
  {
4954
5877
  case 1:
4955
5878
  {
4956
- handlers[0](typedMessage);
5879
+ handlers[0](ref typedMessage);
4957
5880
  return;
4958
5881
  }
4959
5882
  case 2:
4960
5883
  {
4961
- handlers[0](typedMessage);
5884
+ handlers[0](ref typedMessage);
4962
5885
  if (handlers.Count < 2)
4963
5886
  {
4964
5887
  return;
4965
5888
  }
4966
- handlers[1](typedMessage);
5889
+ handlers[1](ref typedMessage);
4967
5890
  return;
4968
5891
  }
4969
5892
  case 3:
4970
5893
  {
4971
- handlers[0](typedMessage);
5894
+ handlers[0](ref typedMessage);
4972
5895
  if (handlers.Count < 2)
4973
5896
  {
4974
5897
  return;
4975
5898
  }
4976
- handlers[1](typedMessage);
5899
+ handlers[1](ref typedMessage);
4977
5900
  if (handlers.Count < 3)
4978
5901
  {
4979
5902
  return;
4980
5903
  }
4981
- handlers[2](typedMessage);
5904
+ handlers[2](ref typedMessage);
4982
5905
  return;
4983
5906
  }
4984
5907
  case 4:
4985
5908
  {
4986
- handlers[0](typedMessage);
5909
+ handlers[0](ref typedMessage);
4987
5910
  if (handlers.Count < 2)
4988
5911
  {
4989
5912
  return;
4990
5913
  }
4991
- handlers[1](typedMessage);
5914
+ handlers[1](ref typedMessage);
4992
5915
  if (handlers.Count < 3)
4993
5916
  {
4994
5917
  return;
4995
5918
  }
4996
- handlers[2](typedMessage);
5919
+ handlers[2](ref typedMessage);
4997
5920
  if (handlers.Count < 4)
4998
5921
  {
4999
5922
  return;
5000
5923
  }
5001
- handlers[3](typedMessage);
5924
+ handlers[3](ref typedMessage);
5002
5925
  return;
5003
5926
  }
5004
5927
  case 5:
5005
5928
  {
5006
- handlers[0](typedMessage);
5929
+ handlers[0](ref typedMessage);
5007
5930
  if (handlers.Count < 2)
5008
5931
  {
5009
5932
  return;
5010
5933
  }
5011
- handlers[1](typedMessage);
5934
+ handlers[1](ref typedMessage);
5012
5935
  if (handlers.Count < 3)
5013
5936
  {
5014
5937
  return;
5015
5938
  }
5016
- handlers[2](typedMessage);
5939
+ handlers[2](ref typedMessage);
5017
5940
  if (handlers.Count < 4)
5018
5941
  {
5019
5942
  return;
5020
5943
  }
5021
- handlers[3](typedMessage);
5944
+ handlers[3](ref typedMessage);
5022
5945
  if (handlers.Count < 5)
5023
5946
  {
5024
5947
  return;
5025
5948
  }
5026
- handlers[4](typedMessage);
5949
+ handlers[4](ref typedMessage);
5027
5950
  return;
5028
5951
  }
5029
5952
  }
5030
5953
 
5031
5954
  for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
5032
5955
  {
5033
- handlers[i](typedMessage);
5956
+ handlers[i](ref typedMessage);
5034
5957
  }
5035
5958
  }
5036
5959
 
@@ -5052,91 +5975,94 @@ namespace DxMessaging.Core
5052
5975
  return;
5053
5976
  }
5054
5977
 
5055
- List<Action<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
5978
+ List<FastHandler<T>> handlers = GetOrAddNewFlatInvokerStack<
5979
+ Action<T>,
5980
+ FastHandler<T>
5981
+ >(cache, emissionId);
5056
5982
  ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5057
5983
  int handlersCount = handlers.Count;
5058
5984
  switch (handlersCount)
5059
5985
  {
5060
5986
  case 1:
5061
5987
  {
5062
- handlers[0](typedMessage);
5988
+ handlers[0](ref typedMessage);
5063
5989
  return;
5064
5990
  }
5065
5991
  case 2:
5066
5992
  {
5067
- handlers[0](typedMessage);
5993
+ handlers[0](ref typedMessage);
5068
5994
  if (handlers.Count < 2)
5069
5995
  {
5070
5996
  return;
5071
5997
  }
5072
- handlers[1](typedMessage);
5998
+ handlers[1](ref typedMessage);
5073
5999
  return;
5074
6000
  }
5075
6001
  case 3:
5076
6002
  {
5077
- handlers[0](typedMessage);
6003
+ handlers[0](ref typedMessage);
5078
6004
  if (handlers.Count < 2)
5079
6005
  {
5080
6006
  return;
5081
6007
  }
5082
- handlers[1](typedMessage);
6008
+ handlers[1](ref typedMessage);
5083
6009
  if (handlers.Count < 3)
5084
6010
  {
5085
6011
  return;
5086
6012
  }
5087
- handlers[2](typedMessage);
6013
+ handlers[2](ref typedMessage);
5088
6014
  return;
5089
6015
  }
5090
6016
  case 4:
5091
6017
  {
5092
- handlers[0](typedMessage);
6018
+ handlers[0](ref typedMessage);
5093
6019
  if (handlers.Count < 2)
5094
6020
  {
5095
6021
  return;
5096
6022
  }
5097
- handlers[1](typedMessage);
6023
+ handlers[1](ref typedMessage);
5098
6024
  if (handlers.Count < 3)
5099
6025
  {
5100
6026
  return;
5101
6027
  }
5102
- handlers[2](typedMessage);
6028
+ handlers[2](ref typedMessage);
5103
6029
  if (handlers.Count < 4)
5104
6030
  {
5105
6031
  return;
5106
6032
  }
5107
- handlers[3](typedMessage);
6033
+ handlers[3](ref typedMessage);
5108
6034
  return;
5109
6035
  }
5110
6036
  case 5:
5111
6037
  {
5112
- handlers[0](typedMessage);
6038
+ handlers[0](ref typedMessage);
5113
6039
  if (handlers.Count < 2)
5114
6040
  {
5115
6041
  return;
5116
6042
  }
5117
- handlers[1](typedMessage);
6043
+ handlers[1](ref typedMessage);
5118
6044
  if (handlers.Count < 3)
5119
6045
  {
5120
6046
  return;
5121
6047
  }
5122
- handlers[2](typedMessage);
6048
+ handlers[2](ref typedMessage);
5123
6049
  if (handlers.Count < 4)
5124
6050
  {
5125
6051
  return;
5126
6052
  }
5127
- handlers[3](typedMessage);
6053
+ handlers[3](ref typedMessage);
5128
6054
  if (handlers.Count < 5)
5129
6055
  {
5130
6056
  return;
5131
6057
  }
5132
- handlers[4](typedMessage);
6058
+ handlers[4](ref typedMessage);
5133
6059
  return;
5134
6060
  }
5135
6061
  }
5136
6062
 
5137
6063
  for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
5138
6064
  {
5139
- handlers[i](typedMessage);
6065
+ handlers[i](ref typedMessage);
5140
6066
  }
5141
6067
  }
5142
6068
 
@@ -5162,94 +6088,94 @@ namespace DxMessaging.Core
5162
6088
  return;
5163
6089
  }
5164
6090
 
5165
- List<Action<InstanceId, T>> typedHandlers = GetOrAddNewHandlerStack(
5166
- cache,
5167
- emissionId
5168
- );
6091
+ List<FastHandlerWithContext<T>> typedHandlers = GetOrAddNewFlatInvokerStack<
6092
+ Action<InstanceId, T>,
6093
+ FastHandlerWithContext<T>
6094
+ >(cache, emissionId);
5169
6095
  ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5170
6096
  int handlersCount = typedHandlers.Count;
5171
6097
  switch (handlersCount)
5172
6098
  {
5173
6099
  case 1:
5174
6100
  {
5175
- typedHandlers[0](context, typedMessage);
6101
+ typedHandlers[0](ref context, ref typedMessage);
5176
6102
  return;
5177
6103
  }
5178
6104
  case 2:
5179
6105
  {
5180
- typedHandlers[0](context, typedMessage);
6106
+ typedHandlers[0](ref context, ref typedMessage);
5181
6107
  if (typedHandlers.Count < 2)
5182
6108
  {
5183
6109
  return;
5184
6110
  }
5185
- typedHandlers[1](context, typedMessage);
6111
+ typedHandlers[1](ref context, ref typedMessage);
5186
6112
  return;
5187
6113
  }
5188
6114
  case 3:
5189
6115
  {
5190
- typedHandlers[0](context, typedMessage);
6116
+ typedHandlers[0](ref context, ref typedMessage);
5191
6117
  if (typedHandlers.Count < 2)
5192
6118
  {
5193
6119
  return;
5194
6120
  }
5195
- typedHandlers[1](context, typedMessage);
6121
+ typedHandlers[1](ref context, ref typedMessage);
5196
6122
  if (typedHandlers.Count < 3)
5197
6123
  {
5198
6124
  return;
5199
6125
  }
5200
- typedHandlers[2](context, typedMessage);
6126
+ typedHandlers[2](ref context, ref typedMessage);
5201
6127
  return;
5202
6128
  }
5203
6129
  case 4:
5204
6130
  {
5205
- typedHandlers[0](context, typedMessage);
6131
+ typedHandlers[0](ref context, ref typedMessage);
5206
6132
  if (typedHandlers.Count < 2)
5207
6133
  {
5208
6134
  return;
5209
6135
  }
5210
- typedHandlers[1](context, typedMessage);
6136
+ typedHandlers[1](ref context, ref typedMessage);
5211
6137
  if (typedHandlers.Count < 3)
5212
6138
  {
5213
6139
  return;
5214
6140
  }
5215
- typedHandlers[2](context, typedMessage);
6141
+ typedHandlers[2](ref context, ref typedMessage);
5216
6142
  if (typedHandlers.Count < 4)
5217
6143
  {
5218
6144
  return;
5219
6145
  }
5220
- typedHandlers[3](context, typedMessage);
6146
+ typedHandlers[3](ref context, ref typedMessage);
5221
6147
  return;
5222
6148
  }
5223
6149
  case 5:
5224
6150
  {
5225
- typedHandlers[0](context, typedMessage);
6151
+ typedHandlers[0](ref context, ref typedMessage);
5226
6152
  if (typedHandlers.Count < 2)
5227
6153
  {
5228
6154
  return;
5229
6155
  }
5230
- typedHandlers[1](context, typedMessage);
6156
+ typedHandlers[1](ref context, ref typedMessage);
5231
6157
  if (typedHandlers.Count < 3)
5232
6158
  {
5233
6159
  return;
5234
6160
  }
5235
- typedHandlers[2](context, typedMessage);
6161
+ typedHandlers[2](ref context, ref typedMessage);
5236
6162
  if (typedHandlers.Count < 4)
5237
6163
  {
5238
6164
  return;
5239
6165
  }
5240
- typedHandlers[3](context, typedMessage);
6166
+ typedHandlers[3](ref context, ref typedMessage);
5241
6167
  if (typedHandlers.Count < 5)
5242
6168
  {
5243
6169
  return;
5244
6170
  }
5245
- typedHandlers[4](context, typedMessage);
6171
+ typedHandlers[4](ref context, ref typedMessage);
5246
6172
  return;
5247
6173
  }
5248
6174
  }
5249
6175
 
5250
6176
  for (int i = 0; i < handlersCount && i < typedHandlers.Count; ++i)
5251
6177
  {
5252
- typedHandlers[i](context, typedMessage);
6178
+ typedHandlers[i](ref context, ref typedMessage);
5253
6179
  }
5254
6180
  }
5255
6181
 
@@ -5277,94 +6203,94 @@ namespace DxMessaging.Core
5277
6203
  return;
5278
6204
  }
5279
6205
 
5280
- List<Action<InstanceId, T>> typedHandlers = GetOrAddNewHandlerStack(
5281
- cache,
5282
- emissionId
5283
- );
6206
+ List<FastHandlerWithContext<T>> typedHandlers = GetOrAddNewFlatInvokerStack<
6207
+ Action<InstanceId, T>,
6208
+ FastHandlerWithContext<T>
6209
+ >(cache, emissionId);
5284
6210
  ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5285
6211
  int handlersCount = typedHandlers.Count;
5286
6212
  switch (handlersCount)
5287
6213
  {
5288
6214
  case 1:
5289
6215
  {
5290
- typedHandlers[0](context, typedMessage);
6216
+ typedHandlers[0](ref context, ref typedMessage);
5291
6217
  return;
5292
6218
  }
5293
6219
  case 2:
5294
6220
  {
5295
- typedHandlers[0](context, typedMessage);
6221
+ typedHandlers[0](ref context, ref typedMessage);
5296
6222
  if (typedHandlers.Count < 2)
5297
6223
  {
5298
6224
  return;
5299
6225
  }
5300
- typedHandlers[1](context, typedMessage);
6226
+ typedHandlers[1](ref context, ref typedMessage);
5301
6227
  return;
5302
6228
  }
5303
6229
  case 3:
5304
6230
  {
5305
- typedHandlers[0](context, typedMessage);
6231
+ typedHandlers[0](ref context, ref typedMessage);
5306
6232
  if (typedHandlers.Count < 2)
5307
6233
  {
5308
6234
  return;
5309
6235
  }
5310
- typedHandlers[1](context, typedMessage);
6236
+ typedHandlers[1](ref context, ref typedMessage);
5311
6237
  if (typedHandlers.Count < 3)
5312
6238
  {
5313
6239
  return;
5314
6240
  }
5315
- typedHandlers[2](context, typedMessage);
6241
+ typedHandlers[2](ref context, ref typedMessage);
5316
6242
  return;
5317
6243
  }
5318
6244
  case 4:
5319
6245
  {
5320
- typedHandlers[0](context, typedMessage);
6246
+ typedHandlers[0](ref context, ref typedMessage);
5321
6247
  if (typedHandlers.Count < 2)
5322
6248
  {
5323
6249
  return;
5324
6250
  }
5325
- typedHandlers[1](context, typedMessage);
6251
+ typedHandlers[1](ref context, ref typedMessage);
5326
6252
  if (typedHandlers.Count < 3)
5327
6253
  {
5328
6254
  return;
5329
6255
  }
5330
- typedHandlers[2](context, typedMessage);
6256
+ typedHandlers[2](ref context, ref typedMessage);
5331
6257
  if (typedHandlers.Count < 4)
5332
6258
  {
5333
6259
  return;
5334
6260
  }
5335
- typedHandlers[3](context, typedMessage);
6261
+ typedHandlers[3](ref context, ref typedMessage);
5336
6262
  return;
5337
6263
  }
5338
6264
  case 5:
5339
6265
  {
5340
- typedHandlers[0](context, typedMessage);
6266
+ typedHandlers[0](ref context, ref typedMessage);
5341
6267
  if (typedHandlers.Count < 2)
5342
6268
  {
5343
6269
  return;
5344
6270
  }
5345
- typedHandlers[1](context, typedMessage);
6271
+ typedHandlers[1](ref context, ref typedMessage);
5346
6272
  if (typedHandlers.Count < 3)
5347
6273
  {
5348
6274
  return;
5349
6275
  }
5350
- typedHandlers[2](context, typedMessage);
6276
+ typedHandlers[2](ref context, ref typedMessage);
5351
6277
  if (typedHandlers.Count < 4)
5352
6278
  {
5353
6279
  return;
5354
6280
  }
5355
- typedHandlers[3](context, typedMessage);
6281
+ typedHandlers[3](ref context, ref typedMessage);
5356
6282
  if (typedHandlers.Count < 5)
5357
6283
  {
5358
6284
  return;
5359
6285
  }
5360
- typedHandlers[4](context, typedMessage);
6286
+ typedHandlers[4](ref context, ref typedMessage);
5361
6287
  return;
5362
6288
  }
5363
6289
  }
5364
6290
 
5365
6291
  for (int i = 0; i < handlersCount && i < typedHandlers.Count; ++i)
5366
6292
  {
5367
- typedHandlers[i](context, typedMessage);
6293
+ typedHandlers[i](ref context, ref typedMessage);
5368
6294
  }
5369
6295
  }
5370
6296
 
@@ -5383,34 +6309,19 @@ namespace DxMessaging.Core
5383
6309
  long emissionId
5384
6310
  )
5385
6311
  {
5386
- DebugAssertInsertionOrderInSync(actionCache);
5387
6312
  if (actionCache.lastSeenEmissionId != emissionId)
5388
6313
  {
5389
6314
  if (actionCache.version != actionCache.lastSeenVersion)
5390
6315
  {
5391
- // Rebuild the dispatch snapshot from insertionOrder, NOT from
5392
- // the entries dictionary: dictionary enumeration order permutes
5393
- // after Remove/Add churn (freed slots are reused LIFO), while
5394
- // insertionOrder preserves the documented first-registration
5395
- // order for equal-priority handlers. This branch only runs on
5396
- // registration churn (version bump), never on steady-state
5397
- // dispatch, and allocates nothing (the pooled cache list is
5398
- // cleared and refilled in place).
6316
+ // Rebuild in the ordered map's first-registration order.
6317
+ // This branch only runs on registration churn, never on
6318
+ // steady-state dispatch, and reuses the snapshot list.
5399
6319
  List<TU> list = actionCache.cache;
5400
6320
  list.Clear();
5401
- List<TU> orderedHandlers = actionCache.insertionOrder;
5402
- int orderedCount = orderedHandlers.Count;
6321
+ int orderedCount = actionCache.entries.Count;
5403
6322
  for (int i = 0; i < orderedCount; ++i)
5404
6323
  {
5405
- if (
5406
- actionCache.entries.TryGetValue(
5407
- orderedHandlers[i],
5408
- out HandlerActionCache<TU>.Entry entry
5409
- )
5410
- )
5411
- {
5412
- list.Add(entry.handler);
5413
- }
6324
+ list.Add(actionCache.entries.ValueAt(i).handler);
5414
6325
  }
5415
6326
  actionCache.lastSeenVersion = actionCache.version;
5416
6327
  }
@@ -5419,32 +6330,57 @@ namespace DxMessaging.Core
5419
6330
  return actionCache.cache;
5420
6331
  }
5421
6332
 
5422
- // Asserts insertionOrder stays in lockstep with the entries
5423
- // dictionary at every dispatch-snapshot read. Drift indicates a
5424
- // mutation site of HandlerActionCache.entries that forgot to
5425
- // mirror the change into insertionOrder (AddHandler* family,
5426
- // deregistration closures, IHandlerActionCache.Reset). Stripped
5427
- // in Release builds via [Conditional("DEBUG")] -- zero hot-path
5428
- // cost.
5429
- [Conditional("DEBUG")]
5430
- private static void DebugAssertInsertionOrderInSync<TU>(
5431
- HandlerActionCache<TU> actionCache
6333
+ // Default-slot registrations may store the raw user Action as the
6334
+ // identity key while carrying diagnostics in Entry.flatInvoker. The
6335
+ // legacy Handle* path must dispatch that same flat invoker snapshot,
6336
+ // not Entry.handler, so diagnostics semantics match bus-side flat
6337
+ // dispatch if the legacy callback path is used directly.
6338
+ internal static List<TInvoker> GetOrAddNewFlatInvokerStack<TU, TInvoker>(
6339
+ HandlerActionCache<TU> actionCache,
6340
+ long emissionId
5432
6341
  )
6342
+ where TInvoker : class
5433
6343
  {
5434
- System.Diagnostics.Debug.Assert(
5435
- actionCache.insertionOrder.Count == actionCache.entries.Count,
5436
- "HandlerActionCache.insertionOrder must mirror entries: every first "
5437
- + "registration appends and every final deregistration removes. A "
5438
- + "count mismatch means a mutation site skipped the insertionOrder "
5439
- + "update and same-priority dispatch order is no longer trustworthy."
5440
- );
6344
+ if (actionCache.flatInvokerLastSeenEmissionId != emissionId)
6345
+ {
6346
+ if (actionCache.version != actionCache.flatInvokerLastSeenVersion)
6347
+ {
6348
+ List<TInvoker> list = actionCache.GetOrCreateFlatInvokerCache<TInvoker>();
6349
+ list.Clear();
6350
+ int orderedCount = actionCache.entries.Count;
6351
+ for (int i = 0; i < orderedCount; ++i)
6352
+ {
6353
+ HandlerActionCache<TU>.Entry entry = actionCache.entries.ValueAt(i);
6354
+
6355
+ if (entry.flatInvoker is TInvoker invoker)
6356
+ {
6357
+ list.Add(invoker);
6358
+ }
6359
+ else
6360
+ {
6361
+ System.Diagnostics.Debug.Assert(
6362
+ false,
6363
+ "Default registration is missing the flat invoker required "
6364
+ + "by legacy Handle* dispatch. Every default Add* path must "
6365
+ + "store the diagnostics-augmented flat invoker in "
6366
+ + "HandlerActionCache.Entry.flatInvoker."
6367
+ );
6368
+ }
6369
+ }
6370
+
6371
+ actionCache.flatInvokerLastSeenVersion = actionCache.version;
6372
+ }
6373
+
6374
+ actionCache.flatInvokerLastSeenEmissionId = emissionId;
6375
+ }
6376
+
6377
+ return actionCache.GetOrCreateFlatInvokerCache<TInvoker>();
5441
6378
  }
5442
6379
 
5443
- private static Action AddHandler<TU>(
6380
+ private static GlobalHandlerDeregistrationState AddHandler<TU>(
5444
6381
  TypedGlobalSlot slot,
5445
6382
  TU originalHandler,
5446
6383
  TU augmentedHandler,
5447
- Action deregistration,
5448
6384
  IMessageBus messageBus
5449
6385
  )
5450
6386
  {
@@ -5473,72 +6409,24 @@ namespace DxMessaging.Core
5473
6409
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5474
6410
 
5475
6411
  cache.entries[originalHandler] = entry;
5476
- if (firstRegistration)
5477
- {
5478
- cache.insertionOrder.Add(originalHandler);
5479
- }
5480
6412
  cache.version++;
5481
6413
  if (firstRegistration)
5482
6414
  {
5483
6415
  slot.liveCount++;
5484
6416
  }
5485
6417
 
5486
- HandlerActionCache<TU> localCache = cache;
5487
- TypedGlobalSlot localSlot = slot;
5488
6418
  long localSlotVersion = slot.version;
5489
6419
  long localResetGeneration =
5490
6420
  global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
5491
6421
 
5492
- return () =>
5493
- {
5494
- if (
5495
- !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
5496
- messageBus,
5497
- localResetGeneration
5498
- )
5499
- )
5500
- {
5501
- return;
5502
- }
5503
-
5504
- if (localSlot.version != localSlotVersion)
5505
- {
5506
- return;
5507
- }
5508
-
5509
- if (
5510
- !localCache.entries.TryGetValue(
5511
- originalHandler,
5512
- out HandlerActionCache<TU>.Entry localEntry
5513
- )
5514
- )
5515
- {
5516
- return;
5517
- }
5518
-
5519
- localCache.version++;
5520
-
5521
- deregistration?.Invoke();
5522
- localSlot.lastTouchTicks =
5523
- global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
5524
- messageBus
5525
- );
5526
-
5527
- if (localEntry.count <= 1)
5528
- {
5529
- _ = localCache.entries.Remove(originalHandler);
5530
- _ = localCache.insertionOrder.Remove(originalHandler);
5531
- localCache.version++;
5532
- localSlot.liveCount--;
5533
- return;
5534
- }
5535
-
5536
- localEntry = new HandlerActionCache<TU>.Entry(
5537
- localEntry.handler,
5538
- localEntry.count - 1
5539
- );
5540
- localCache.entries[originalHandler] = localEntry;
5541
- };
6422
+ return new GlobalHandlerDeregistrationState(
6423
+ messageBus,
6424
+ localResetGeneration,
6425
+ slot,
6426
+ localSlotVersion,
6427
+ cache,
6428
+ originalHandler
6429
+ );
5542
6430
  }
5543
6431
 
5544
6432
  private static Action AddHandler<TU>(
@@ -5590,10 +6478,6 @@ namespace DxMessaging.Core
5590
6478
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5591
6479
 
5592
6480
  cache.entries[originalHandler] = entry;
5593
- if (firstRegistration)
5594
- {
5595
- cache.insertionOrder.Add(originalHandler);
5596
- }
5597
6481
  cache.version++;
5598
6482
 
5599
6483
  Dictionary<
@@ -5632,7 +6516,6 @@ namespace DxMessaging.Core
5632
6516
  if (localEntry.count <= 1)
5633
6517
  {
5634
6518
  _ = localCache.entries.Remove(originalHandler);
5635
- _ = localCache.insertionOrder.Remove(originalHandler);
5636
6519
  localCache.version++;
5637
6520
  if (localCache.entries.Count == 0)
5638
6521
  {
@@ -5680,10 +6563,6 @@ namespace DxMessaging.Core
5680
6563
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5681
6564
 
5682
6565
  cache.entries[originalHandler] = entry;
5683
- if (firstRegistration)
5684
- {
5685
- cache.insertionOrder.Add(originalHandler);
5686
- }
5687
6566
  cache.version++;
5688
6567
 
5689
6568
  HandlerActionCache<TU> localCache = cache;
@@ -5707,7 +6586,6 @@ namespace DxMessaging.Core
5707
6586
  if (localEntry.count <= 1)
5708
6587
  {
5709
6588
  _ = localCache.entries.Remove(originalHandler);
5710
- _ = localCache.insertionOrder.Remove(originalHandler);
5711
6589
  localCache.version++;
5712
6590
  return;
5713
6591
  }
@@ -5753,10 +6631,6 @@ namespace DxMessaging.Core
5753
6631
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5754
6632
 
5755
6633
  cache.entries[originalHandler] = entry;
5756
- if (firstRegistration)
5757
- {
5758
- cache.insertionOrder.Add(originalHandler);
5759
- }
5760
6634
  cache.version++;
5761
6635
 
5762
6636
  Dictionary<int, HandlerActionCache<TU>> localHandlers = handlers;
@@ -5785,7 +6659,6 @@ namespace DxMessaging.Core
5785
6659
  if (localEntry.count <= 1)
5786
6660
  {
5787
6661
  _ = localCache.entries.Remove(originalHandler);
5788
- _ = localCache.insertionOrder.Remove(originalHandler);
5789
6662
  localCache.version++;
5790
6663
  if (localCache.entries.Count == 0)
5791
6664
  {
@@ -5810,11 +6683,11 @@ namespace DxMessaging.Core
5810
6683
  // `flatInvoker` carries the pre-resolved flat-dispatch invoker for
5811
6684
  // registrations the bus-side flat snapshot consumes (untargeted
5812
6685
  // handle/post default handlers); see HandlerActionCache.Entry.flatInvoker.
5813
- private Action AddHandlerPreservingPriorityKey<TU>(
6686
+ private TypedHandlerDeregistrationState AddHandlerPreservingPriorityKey<TU>(
5814
6687
  Dictionary<int, IHandlerActionCache> handlers,
5815
6688
  TU originalHandler,
5816
6689
  TU augmentedHandler,
5817
- Action deregistration,
6690
+ MessageBusRegistration deregistration,
5818
6691
  int priority,
5819
6692
  IMessageBus messageBus,
5820
6693
  object flatInvoker = null
@@ -5849,10 +6722,6 @@ namespace DxMessaging.Core
5849
6722
  );
5850
6723
 
5851
6724
  cache.entries[originalHandler] = entry;
5852
- if (firstRegistration)
5853
- {
5854
- cache.insertionOrder.Add(originalHandler);
5855
- }
5856
6725
  cache.version++;
5857
6726
  TypedSlot<T> slot = FindPrioritySlot(handlers);
5858
6727
  if (slot != null)
@@ -5871,82 +6740,22 @@ namespace DxMessaging.Core
5871
6740
  slot.liveCount++;
5872
6741
  }
5873
6742
 
5874
- Dictionary<int, IHandlerActionCache> localHandlers = handlers;
5875
- TypedSlot<T> localSlot = slot;
5876
6743
  long localSlotVersion = slot?.version ?? 0;
5877
6744
  long localResetGeneration =
5878
6745
  global::DxMessaging.Core.MessageBus.MessageBus.GetResetGeneration(messageBus);
5879
6746
 
5880
- return () =>
5881
- {
5882
- if (
5883
- !global::DxMessaging.Core.MessageBus.MessageBus.IsResetGenerationCurrent(
5884
- messageBus,
5885
- localResetGeneration
5886
- )
5887
- )
5888
- {
5889
- return;
5890
- }
5891
-
5892
- if (localSlot != null && localSlot.version != localSlotVersion)
5893
- {
5894
- return;
5895
- }
5896
-
5897
- if (
5898
- !localHandlers.TryGetValue(
5899
- priority,
5900
- out IHandlerActionCache localErasedCache
5901
- ) || localErasedCache is not HandlerActionCache<TU> localCache
5902
- )
5903
- {
5904
- return;
5905
- }
5906
-
5907
- if (
5908
- !localCache.entries.TryGetValue(
5909
- originalHandler,
5910
- out HandlerActionCache<TU>.Entry localEntry
5911
- )
5912
- )
5913
- {
5914
- return;
5915
- }
5916
-
5917
- localCache.version++;
5918
-
5919
- deregistration?.Invoke();
5920
- if (localSlot != null)
5921
- {
5922
- localSlot.lastTouchTicks =
5923
- global::DxMessaging.Core.MessageBus.MessageBus.GetCurrentTouchTick(
5924
- messageBus
5925
- );
5926
- }
5927
-
5928
- if (localEntry.count <= 1)
5929
- {
5930
- _ = localCache.entries.Remove(originalHandler);
5931
- _ = localCache.insertionOrder.Remove(originalHandler);
5932
- localCache.version++;
5933
- if (localSlot != null)
5934
- {
5935
- localSlot.liveCount--;
5936
- }
5937
- // Intentionally DO NOT remove the priority key here to preserve
5938
- // the cache handle during an in-flight emission.
5939
- return;
5940
- }
5941
-
5942
- localEntry = new HandlerActionCache<TU>.Entry(
5943
- localEntry.handler,
5944
- localEntry.count - 1,
5945
- localEntry.flatInvoker
5946
- );
5947
-
5948
- localCache.entries[originalHandler] = localEntry;
5949
- };
6747
+ // See the context overload: this re-expresses the old scalar
6748
+ // deregistration closure verbatim as a per-handle object.
6749
+ return new TypedHandlerDeregistrationState(
6750
+ messageBus,
6751
+ localResetGeneration,
6752
+ slot,
6753
+ localSlotVersion,
6754
+ handlers,
6755
+ originalHandler,
6756
+ priority,
6757
+ in deregistration
6758
+ );
5950
6759
  }
5951
6760
 
5952
6761
  private static Action AddHandlerPreservingPriorityKey<TU>(
@@ -5982,10 +6791,6 @@ namespace DxMessaging.Core
5982
6791
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5983
6792
 
5984
6793
  cache.entries[originalHandler] = entry;
5985
- if (firstRegistration)
5986
- {
5987
- cache.insertionOrder.Add(originalHandler);
5988
- }
5989
6794
  cache.version++;
5990
6795
 
5991
6796
  Dictionary<int, HandlerActionCache<TU>> localHandlers = handlers;
@@ -6014,7 +6819,6 @@ namespace DxMessaging.Core
6014
6819
  if (localEntry.count <= 1)
6015
6820
  {
6016
6821
  _ = localCache.entries.Remove(originalHandler);
6017
- _ = localCache.insertionOrder.Remove(originalHandler);
6018
6822
  localCache.version++;
6019
6823
  // Intentionally DO NOT remove the priority key here to preserve
6020
6824
  // the cache handle during an in-flight emission.