com.wallstop-studios.dxmessaging 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +211 -2
  2. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +69 -62
  3. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +3 -3
  4. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +3 -3
  5. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +3 -3
  6. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +3 -3
  7. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +3 -3
  8. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  9. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +15 -2
  10. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  11. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +2 -2
  12. package/Editor/AssemblyInfo.cs.meta +9 -1
  13. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +24 -15
  14. package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +9 -1
  15. package/Editor/DxMessagingEditorIdle.cs +62 -0
  16. package/{Runtime/Core/Internal/TypedDispatchLinkIndex.cs.meta → Editor/DxMessagingEditorIdle.cs.meta} +1 -1
  17. package/Editor/DxMessagingEditorInitializer.cs +112 -15
  18. package/Editor/DxMessagingEditorInitializer.cs.meta +9 -1
  19. package/Editor/DxMessagingEditorLog.cs +32 -0
  20. package/Editor/DxMessagingEditorLog.cs.meta +11 -0
  21. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +135 -12
  22. package/Editor/Settings/DxMessagingSettings.cs +92 -31
  23. package/Editor/Settings/DxMessagingSettings.cs.meta +9 -1
  24. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +9 -1
  25. package/Editor/SetupCscRsp.cs +339 -173
  26. package/Editor/SetupCscRsp.cs.meta +9 -1
  27. package/Editor/Testing/MessagingComponentEditorHarness.cs +1 -1
  28. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +9 -1
  29. package/README.md +17 -18
  30. package/Runtime/AssemblyInfo.cs.meta +9 -1
  31. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +9 -1
  32. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +9 -1
  33. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs +2 -4
  34. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +9 -1
  35. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +9 -1
  36. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +9 -1
  37. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs +56 -0
  38. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs.meta +11 -0
  39. package/Runtime/Core/DataStructure/CyclicBuffer.cs +44 -26
  40. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +9 -1
  41. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +9 -1
  42. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +9 -1
  43. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +9 -1
  44. package/Runtime/Core/Extensions/EnumExtensions.cs +6 -5
  45. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +9 -1
  46. package/Runtime/Core/Extensions/IListExtensions.cs.meta +9 -1
  47. package/Runtime/Core/Extensions/MessageExtensions.cs +0 -60
  48. package/Runtime/Core/Helper/MessageCache.cs.meta +9 -1
  49. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +9 -1
  50. package/Runtime/Core/InstanceId.cs +25 -1
  51. package/Runtime/Core/Internal/DxUnsafe.cs +60 -0
  52. package/Runtime/Core/Internal/DxUnsafe.cs.meta +11 -0
  53. package/Runtime/Core/Internal/FlatDispatch.cs +198 -0
  54. package/Runtime/Core/Internal/FlatDispatch.cs.meta +11 -0
  55. package/Runtime/Core/Internal/TypedSlots.cs +5 -21
  56. package/Runtime/Core/MessageBus/IMessageBus.cs +12 -12
  57. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs +1 -0
  58. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +7 -6
  59. package/Runtime/Core/MessageBus/MessageBus.cs +2313 -2936
  60. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs +187 -14
  61. package/Runtime/Core/MessageHandler.cs +1023 -1143
  62. package/Runtime/Core/MessageRegistrationToken.cs +425 -47
  63. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +9 -1
  64. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +9 -1
  65. package/Runtime/Core/Messages/StringMessage.cs.meta +9 -1
  66. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +9 -1
  67. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +9 -1
  68. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +9 -1
  69. package/Runtime/Unity/MessageAwareComponent.cs +46 -1
  70. package/Runtime/Unity/MessagingComponent.cs +43 -10
  71. package/Runtime/WallstopStudios.DxMessaging.asmdef +1 -1
  72. package/Samples~/DI/README.md +7 -7
  73. package/SourceGenerators/Directory.Build.props +50 -3
  74. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs +96 -63
  75. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +745 -87
  76. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +9 -1
  77. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +39 -46
  78. package/SourceGenerators/global.json +7 -0
  79. package/SourceGenerators/global.json.meta +7 -0
  80. package/package.json +27 -40
  81. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs +0 -51
@@ -37,413 +37,6 @@ namespace DxMessaging.Core
37
37
  IComparable,
38
38
  IComparable<MessageHandler>
39
39
  {
40
- private static void PrefreezePriorityCache<TMessage, THandler>(
41
- TypedHandler<TMessage> handler,
42
- int slotIndex,
43
- int priority,
44
- long emissionId
45
- )
46
- where TMessage : IMessage
47
- {
48
- Dictionary<int, IHandlerActionCache> byPriority = handler.GetPriorityHandlers(
49
- slotIndex
50
- );
51
- if (
52
- byPriority != null
53
- && byPriority.TryGetValue(priority, out IHandlerActionCache erasedCache)
54
- && erasedCache is HandlerActionCache<THandler> cache
55
- )
56
- {
57
- _ = TypedHandler<TMessage>.GetOrAddNewHandlerStack(cache, emissionId);
58
- cache.prefreezeInvocationCount++;
59
- }
60
- }
61
-
62
- private static void PrefreezeContextCache<TMessage, THandler>(
63
- TypedHandler<TMessage> handler,
64
- int slotIndex,
65
- InstanceId context,
66
- int priority,
67
- long emissionId
68
- )
69
- where TMessage : IMessage
70
- {
71
- Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> byContext =
72
- handler.GetContextHandlers(slotIndex);
73
- if (
74
- byContext != null
75
- && byContext.TryGetValue(
76
- context,
77
- out Dictionary<int, IHandlerActionCache> byPriority
78
- )
79
- && byPriority.TryGetValue(priority, out IHandlerActionCache erasedCache)
80
- && erasedCache is HandlerActionCache<THandler> cache
81
- )
82
- {
83
- _ = TypedHandler<TMessage>.GetOrAddNewHandlerStack(cache, emissionId);
84
- cache.prefreezeInvocationCount++;
85
- }
86
- }
87
-
88
- private static int GetPriorityPrefreezeInvocationCount<TMessage, THandler>(
89
- TypedHandler<TMessage> handler,
90
- int slotIndex,
91
- int priority
92
- )
93
- where TMessage : IMessage
94
- {
95
- Dictionary<int, IHandlerActionCache> byPriority = handler.GetPriorityHandlers(
96
- slotIndex
97
- );
98
- if (
99
- byPriority != null
100
- && byPriority.TryGetValue(priority, out IHandlerActionCache erasedCache)
101
- && erasedCache is HandlerActionCache<THandler> cache
102
- )
103
- {
104
- return cache.prefreezeInvocationCount;
105
- }
106
-
107
- return 0;
108
- }
109
-
110
- /// <summary>
111
- /// Pre-freezes this handler's broadcast post-processor caches for the given message type, source, and priority
112
- /// for the specified emission id, so registrations during the same emission are not observed.
113
- /// </summary>
114
- /// <typeparam name="T">Broadcast message type.</typeparam>
115
- /// <param name="source">Source instance id.</param>
116
- /// <param name="priority">Priority bucket to freeze.</param>
117
- /// <param name="emissionId">Current emission id.</param>
118
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
119
- internal void PrefreezeBroadcastPostProcessorsForEmission<T>(
120
- InstanceId source,
121
- int priority,
122
- long emissionId,
123
- IMessageBus messageBus
124
- )
125
- where T : IBroadcastMessage
126
- {
127
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
128
- {
129
- return;
130
- }
131
-
132
- PrefreezeContextCache<T, FastHandler<T>>(
133
- handler,
134
- TypedSlotIndex.BroadcastPostProcessFast,
135
- source,
136
- priority,
137
- emissionId
138
- );
139
- PrefreezeContextCache<T, Action<T>>(
140
- handler,
141
- TypedSlotIndex.BroadcastPostProcessDefault,
142
- source,
143
- priority,
144
- emissionId
145
- );
146
- }
147
-
148
- /// <summary>
149
- /// Pre-freezes this handler's targeted post-processor caches for the given message type, target, and priority
150
- /// for the specified emission id, so registrations during the same emission are not observed.
151
- /// </summary>
152
- /// <typeparam name="T">Targeted message type.</typeparam>
153
- /// <param name="target">Target instance id.</param>
154
- /// <param name="priority">Priority bucket to freeze.</param>
155
- /// <param name="emissionId">Current emission id.</param>
156
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
157
- internal void PrefreezeTargetedPostProcessorsForEmission<T>(
158
- InstanceId target,
159
- int priority,
160
- long emissionId,
161
- IMessageBus messageBus
162
- )
163
- where T : ITargetedMessage
164
- {
165
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
166
- {
167
- return;
168
- }
169
-
170
- PrefreezeContextCache<T, FastHandler<T>>(
171
- handler,
172
- TypedSlotIndex.TargetedPostProcessFast,
173
- target,
174
- priority,
175
- emissionId
176
- );
177
- PrefreezeContextCache<T, Action<T>>(
178
- handler,
179
- TypedSlotIndex.TargetedPostProcessDefault,
180
- target,
181
- priority,
182
- emissionId
183
- );
184
- }
185
-
186
- /// <summary>
187
- /// Pre-freezes this handler's targeted-without-targeting handler caches for the given message type and priority
188
- /// so that removals/additions during the same emission are not observed.
189
- /// </summary>
190
- internal void PrefreezeTargetedWithoutTargetingHandlersForEmission<T>(
191
- int priority,
192
- long emissionId,
193
- IMessageBus messageBus
194
- )
195
- where T : ITargetedMessage
196
- {
197
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
198
- {
199
- return;
200
- }
201
-
202
- PrefreezePriorityCache<T, FastHandlerWithContext<T>>(
203
- handler,
204
- TypedSlotIndex.TargetedHandleWithoutContextFast,
205
- priority,
206
- emissionId
207
- );
208
- PrefreezePriorityCache<T, Action<InstanceId, T>>(
209
- handler,
210
- TypedSlotIndex.TargetedHandleWithoutContext,
211
- priority,
212
- emissionId
213
- );
214
- }
215
-
216
- /// <summary>
217
- /// Pre-freezes this handler's targeted-without-targeting post-processor caches for a given priority.
218
- /// </summary>
219
- internal void PrefreezeTargetedWithoutTargetingPostProcessorsForEmission<T>(
220
- int priority,
221
- long emissionId,
222
- IMessageBus messageBus
223
- )
224
- where T : ITargetedMessage
225
- {
226
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
227
- {
228
- return;
229
- }
230
-
231
- PrefreezePriorityCache<T, FastHandlerWithContext<T>>(
232
- handler,
233
- TypedSlotIndex.TargetedPostProcessWithoutContextFast,
234
- priority,
235
- emissionId
236
- );
237
- PrefreezePriorityCache<T, Action<InstanceId, T>>(
238
- handler,
239
- TypedSlotIndex.TargetedPostProcessWithoutContext,
240
- priority,
241
- emissionId
242
- );
243
- }
244
-
245
- /// <summary>
246
- /// Pre-freezes this handler's untargeted post-processor caches for a given priority.
247
- /// </summary>
248
- internal void PrefreezeUntargetedPostProcessorsForEmission<T>(
249
- int priority,
250
- long emissionId,
251
- IMessageBus messageBus
252
- )
253
- where T : IUntargetedMessage
254
- {
255
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
256
- {
257
- return;
258
- }
259
-
260
- PrefreezePriorityCache<T, FastHandler<T>>(
261
- handler,
262
- TypedSlotIndex.UntargetedPostProcessFast,
263
- priority,
264
- emissionId
265
- );
266
- PrefreezePriorityCache<T, Action<T>>(
267
- handler,
268
- TypedSlotIndex.UntargetedPostProcessDefault,
269
- priority,
270
- emissionId
271
- );
272
- }
273
-
274
- /// <summary>
275
- /// Pre-freezes this handler's broadcast-without-source post-processor caches for a given priority.
276
- /// </summary>
277
- internal void PrefreezeBroadcastWithoutSourcePostProcessorsForEmission<T>(
278
- int priority,
279
- long emissionId,
280
- IMessageBus messageBus
281
- )
282
- where T : IBroadcastMessage
283
- {
284
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
285
- {
286
- return;
287
- }
288
-
289
- PrefreezePriorityCache<T, FastHandlerWithContext<T>>(
290
- handler,
291
- TypedSlotIndex.BroadcastPostProcessWithoutContextFast,
292
- priority,
293
- emissionId
294
- );
295
- PrefreezePriorityCache<T, Action<InstanceId, T>>(
296
- handler,
297
- TypedSlotIndex.BroadcastPostProcessWithoutContext,
298
- priority,
299
- emissionId
300
- );
301
- }
302
-
303
- /// <summary>
304
- /// Pre-freezes this handler's broadcast-without-source handler caches for the given message type and priority
305
- /// for the specified emission id, so removals during the same emission are not observed.
306
- /// </summary>
307
- /// <typeparam name="T">Broadcast message type.</typeparam>
308
- /// <param name="priority">Priority bucket to freeze.</param>
309
- /// <param name="emissionId">Current emission id.</param>
310
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
311
- internal void PrefreezeBroadcastWithoutSourceHandlersForEmission<T>(
312
- int priority,
313
- long emissionId,
314
- IMessageBus messageBus
315
- )
316
- where T : IBroadcastMessage
317
- {
318
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
319
- {
320
- return;
321
- }
322
-
323
- PrefreezePriorityCache<T, FastHandlerWithContext<T>>(
324
- handler,
325
- TypedSlotIndex.BroadcastHandleWithoutContextFast,
326
- priority,
327
- emissionId
328
- );
329
- PrefreezePriorityCache<T, Action<InstanceId, T>>(
330
- handler,
331
- TypedSlotIndex.BroadcastHandleWithoutContext,
332
- priority,
333
- emissionId
334
- );
335
- }
336
-
337
- /// <summary>
338
- /// Pre-freezes this handler's untargeted handler caches for the given message type and priority
339
- /// for the specified emission id, so removals during the same emission are not observed.
340
- /// </summary>
341
- /// <typeparam name="T">Untargeted message type.</typeparam>
342
- /// <param name="priority">Priority bucket to freeze.</param>
343
- /// <param name="emissionId">Current emission id.</param>
344
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
345
- internal void PrefreezeUntargetedHandlersForEmission<T>(
346
- int priority,
347
- long emissionId,
348
- IMessageBus messageBus
349
- )
350
- where T : IUntargetedMessage
351
- {
352
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
353
- {
354
- return;
355
- }
356
-
357
- PrefreezePriorityCache<T, FastHandler<T>>(
358
- handler,
359
- TypedSlotIndex.UntargetedHandleFast,
360
- priority,
361
- emissionId
362
- );
363
- PrefreezePriorityCache<T, Action<T>>(
364
- handler,
365
- TypedSlotIndex.UntargetedHandleDefault,
366
- priority,
367
- emissionId
368
- );
369
- }
370
-
371
- /// <summary>
372
- /// Pre-freezes this handler's targeted handler caches for the given message type, target, and priority
373
- /// for the specified emission id, so removals during the same emission are not observed.
374
- /// </summary>
375
- /// <typeparam name="T">Targeted message type.</typeparam>
376
- /// <param name="target">Target instance id.</param>
377
- /// <param name="priority">Priority bucket to freeze.</param>
378
- /// <param name="emissionId">Current emission id.</param>
379
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
380
- internal void PrefreezeTargetedHandlersForEmission<T>(
381
- InstanceId target,
382
- int priority,
383
- long emissionId,
384
- IMessageBus messageBus
385
- )
386
- where T : ITargetedMessage
387
- {
388
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
389
- {
390
- return;
391
- }
392
-
393
- PrefreezeContextCache<T, FastHandler<T>>(
394
- handler,
395
- TypedSlotIndex.TargetedHandleFast,
396
- target,
397
- priority,
398
- emissionId
399
- );
400
- PrefreezeContextCache<T, Action<T>>(
401
- handler,
402
- TypedSlotIndex.TargetedHandleDefault,
403
- target,
404
- priority,
405
- emissionId
406
- );
407
- }
408
-
409
- /// <summary>
410
- /// Pre-freezes this handler's broadcast handler caches for the given message type, source, and priority
411
- /// for the specified emission id, so removals during the same emission are not observed.
412
- /// </summary>
413
- /// <typeparam name="T">Broadcast message type.</typeparam>
414
- /// <param name="source">Source instance id.</param>
415
- /// <param name="priority">Priority bucket to freeze.</param>
416
- /// <param name="emissionId">Current emission id.</param>
417
- /// <param name="messageBus">Bus whose typed handler mapping to use.</param>
418
- internal void PrefreezeBroadcastHandlersForEmission<T>(
419
- InstanceId source,
420
- int priority,
421
- long emissionId,
422
- IMessageBus messageBus
423
- )
424
- where T : IBroadcastMessage
425
- {
426
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
427
- {
428
- return;
429
- }
430
-
431
- PrefreezeContextCache<T, FastHandler<T>>(
432
- handler,
433
- TypedSlotIndex.BroadcastHandleFast,
434
- source,
435
- priority,
436
- emissionId
437
- );
438
- PrefreezeContextCache<T, Action<T>>(
439
- handler,
440
- TypedSlotIndex.BroadcastHandleDefault,
441
- source,
442
- priority,
443
- emissionId
444
- );
445
- }
446
-
447
40
  /// <summary>
448
41
  /// High-performance handler that receives the message by reference (no boxing/copies).
449
42
  /// </summary>
@@ -2092,7 +1685,7 @@ namespace DxMessaging.Core
2092
1685
  MessageCache<object> handlersByType = _handlersByTypeByMessageBus[messageBusIndex];
2093
1686
  if (handlersByType.TryGetValue<T>(out object untypedHandler))
2094
1687
  {
2095
- return Unsafe.As<TypedHandler<T>>(untypedHandler);
1688
+ return DxUnsafe.As<TypedHandler<T>>(untypedHandler);
2096
1689
  }
2097
1690
 
2098
1691
  TypedHandler<T> typedHandler = new();
@@ -2124,7 +1717,7 @@ namespace DxMessaging.Core
2124
1717
  .TryGetValue<T>(out object untypedHandler)
2125
1718
  )
2126
1719
  {
2127
- existingTypedHandler = Unsafe.As<TypedHandler<T>>(untypedHandler);
1720
+ existingTypedHandler = DxUnsafe.As<TypedHandler<T>>(untypedHandler);
2128
1721
  return true;
2129
1722
  }
2130
1723
 
@@ -2230,601 +1823,538 @@ namespace DxMessaging.Core
2230
1823
  return false;
2231
1824
  }
2232
1825
 
2233
- internal int GetUntargetedPostProcessingPrefreezeCount<T>(
1826
+ /// <summary>
1827
+ /// Counts the flat-dispatch entries this handler contributes to a
1828
+ /// non-context-keyed slot (untargeted handle/post) at the given
1829
+ /// priority: one entry per unique registered delegate (fast plus
1830
+ /// default). Build-time only; called by MessageBus.BuildFlatDispatch.
1831
+ /// </summary>
1832
+ internal int CountFlatHandlers<T>(
2234
1833
  IMessageBus messageBus,
2235
- int priority
1834
+ int priority,
1835
+ int fastIndex,
1836
+ int defaultIndex
2236
1837
  )
2237
1838
  where T : IMessage
2238
1839
  {
2239
- if (!GetHandlerForType(messageBus, out TypedHandler<T> handler))
1840
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
2240
1841
  {
2241
1842
  return 0;
2242
1843
  }
2243
1844
 
2244
- return GetPriorityPrefreezeInvocationCount<T, FastHandler<T>>(
2245
- handler,
2246
- TypedSlotIndex.UntargetedPostProcessFast,
2247
- priority
2248
- );
1845
+ return CountFlatDelegates<FastHandler<T>>(
1846
+ typedHandler.GetPriorityHandlers(fastIndex),
1847
+ priority
1848
+ )
1849
+ + CountFlatDelegates<Action<T>>(
1850
+ typedHandler.GetPriorityHandlers(defaultIndex),
1851
+ priority
1852
+ );
2249
1853
  }
2250
1854
 
2251
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2252
- internal UntargetedDispatchLink<T> GetOrCreateUntargetedDispatchLink<T>(
2253
- IMessageBus messageBus
1855
+ /// <summary>
1856
+ /// Counts the flat-dispatch entries this handler contributes to a
1857
+ /// context-keyed slot (Default-variant targeted/broadcast handle/post)
1858
+ /// for the given context and priority. Build-time only.
1859
+ /// </summary>
1860
+ internal int CountContextFlatHandlers<T>(
1861
+ IMessageBus messageBus,
1862
+ InstanceId context,
1863
+ int priority,
1864
+ int fastIndex,
1865
+ int defaultIndex
2254
1866
  )
2255
1867
  where T : IMessage
2256
1868
  {
2257
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2258
- return typedHandler.GetOrCreateUntargetedLink();
1869
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1870
+ {
1871
+ return 0;
1872
+ }
1873
+
1874
+ return CountFlatDelegates<FastHandler<T>>(
1875
+ GetContextPriorityHandlers(typedHandler, fastIndex, context),
1876
+ priority
1877
+ )
1878
+ + CountFlatDelegates<Action<T>>(
1879
+ GetContextPriorityHandlers(typedHandler, defaultIndex, context),
1880
+ priority
1881
+ );
2259
1882
  }
2260
1883
 
2261
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2262
- internal UntargetedPostDispatchLink<T> GetOrCreateUntargetedPostDispatchLink<T>(
2263
- IMessageBus messageBus
1884
+ /// <summary>
1885
+ /// Counts the flat-dispatch entries this handler contributes to a
1886
+ /// WithoutContext targeted/broadcast slot (whose delegates receive the
1887
+ /// routing InstanceId) at the given priority. Build-time only.
1888
+ /// </summary>
1889
+ internal int CountWithContextFlatHandlers<T>(
1890
+ IMessageBus messageBus,
1891
+ int priority,
1892
+ int fastIndex,
1893
+ int defaultIndex
2264
1894
  )
2265
1895
  where T : IMessage
2266
1896
  {
2267
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2268
- return typedHandler.GetOrCreateUntargetedPostLink();
2269
- }
1897
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1898
+ {
1899
+ return 0;
1900
+ }
2270
1901
 
2271
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2272
- internal TargetedDispatchLink<T> GetOrCreateTargetedDispatchLink<T>(IMessageBus messageBus)
2273
- where T : IMessage
2274
- {
2275
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2276
- return typedHandler.GetOrCreateTargetedLink();
1902
+ return CountFlatDelegates<FastHandlerWithContext<T>>(
1903
+ typedHandler.GetPriorityHandlers(fastIndex),
1904
+ priority
1905
+ )
1906
+ + CountFlatDelegates<Action<InstanceId, T>>(
1907
+ typedHandler.GetPriorityHandlers(defaultIndex),
1908
+ priority
1909
+ );
2277
1910
  }
2278
1911
 
2279
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2280
- internal TargetedPostDispatchLink<T> GetOrCreateTargetedPostDispatchLink<T>(
2281
- IMessageBus messageBus
1912
+ /// <summary>
1913
+ /// Writes this handler's resolved flat-dispatch entries for a
1914
+ /// non-context-keyed FastHandler-shaped slot into
1915
+ /// <paramref name="target"/> starting at <paramref name="writeIndex"/>:
1916
+ /// all fast entries in registration order, then all default entries in
1917
+ /// registration order, matching the legacy link path's
1918
+ /// fast-before-default contract. Fast entries resolve to the augmented
1919
+ /// FastHandler delegate itself; default entries resolve to the
1920
+ /// FastHandler adapter created at registration time
1921
+ /// (Entry.flatInvoker), so this method allocates nothing.
1922
+ /// Returns the next write index.
1923
+ /// </summary>
1924
+ internal int FillFlatHandlers<T>(
1925
+ IMessageBus messageBus,
1926
+ int priority,
1927
+ int fastIndex,
1928
+ int defaultIndex,
1929
+ FlatDispatchEntry<T>[] target,
1930
+ int writeIndex
2282
1931
  )
2283
1932
  where T : IMessage
2284
1933
  {
2285
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2286
- return typedHandler.GetOrCreateTargetedPostLink();
2287
- }
1934
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1935
+ {
1936
+ return writeIndex;
1937
+ }
2288
1938
 
2289
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2290
- internal TargetedWithoutTargetingDispatchLink<T> GetOrCreateTargetedWithoutTargetingDispatchLink<T>(
2291
- IMessageBus messageBus
2292
- )
2293
- where T : IMessage
2294
- {
2295
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2296
- return typedHandler.GetOrCreateTargetedWithoutTargetingLink();
1939
+ writeIndex = FillFastFlatEntries(
1940
+ typedHandler.GetPriorityHandlers(fastIndex),
1941
+ priority,
1942
+ target,
1943
+ writeIndex
1944
+ );
1945
+ return FillDefaultFlatEntries(
1946
+ typedHandler.GetPriorityHandlers(defaultIndex),
1947
+ priority,
1948
+ target,
1949
+ writeIndex
1950
+ );
2297
1951
  }
2298
1952
 
2299
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2300
- internal TargetedWithoutTargetingPostDispatchLink<T> GetOrCreateTargetedWithoutTargetingPostDispatchLink<T>(
2301
- IMessageBus messageBus
1953
+ /// <summary>
1954
+ /// Context-keyed sibling of <see cref="FillFlatHandlers{T}"/> for the
1955
+ /// Default-variant targeted/broadcast slots: resolves the per-context
1956
+ /// priority map first, then fills fast entries followed by default
1957
+ /// entries in registration order. Returns the next write index.
1958
+ /// </summary>
1959
+ internal int FillContextFlatHandlers<T>(
1960
+ IMessageBus messageBus,
1961
+ InstanceId context,
1962
+ int priority,
1963
+ int fastIndex,
1964
+ int defaultIndex,
1965
+ FlatDispatchEntry<T>[] target,
1966
+ int writeIndex
2302
1967
  )
2303
1968
  where T : IMessage
2304
1969
  {
2305
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2306
- return typedHandler.GetOrCreateTargetedWithoutTargetingPostLink();
2307
- }
1970
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
1971
+ {
1972
+ return writeIndex;
1973
+ }
2308
1974
 
2309
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2310
- internal BroadcastDispatchLink<T> GetOrCreateBroadcastDispatchLink<T>(
2311
- IMessageBus messageBus
2312
- )
2313
- where T : IMessage
2314
- {
2315
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2316
- return typedHandler.GetOrCreateBroadcastLink();
1975
+ writeIndex = FillFastFlatEntries(
1976
+ GetContextPriorityHandlers(typedHandler, fastIndex, context),
1977
+ priority,
1978
+ target,
1979
+ writeIndex
1980
+ );
1981
+ return FillDefaultFlatEntries(
1982
+ GetContextPriorityHandlers(typedHandler, defaultIndex, context),
1983
+ priority,
1984
+ target,
1985
+ writeIndex
1986
+ );
2317
1987
  }
2318
1988
 
2319
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2320
- internal BroadcastPostDispatchLink<T> GetOrCreateBroadcastPostDispatchLink<T>(
2321
- IMessageBus messageBus
1989
+ /// <summary>
1990
+ /// WithoutContext sibling of <see cref="FillFlatHandlers{T}"/> for the
1991
+ /// targeted/broadcast slots whose delegates receive the routing
1992
+ /// InstanceId: fills fast (FastHandlerWithContext) entries followed by
1993
+ /// default (Action&lt;InstanceId, T&gt;, via the registration-time
1994
+ /// FastHandlerWithContext adapter) entries in registration order.
1995
+ /// Returns the next write index.
1996
+ /// </summary>
1997
+ internal int FillWithContextFlatHandlers<T>(
1998
+ IMessageBus messageBus,
1999
+ int priority,
2000
+ int fastIndex,
2001
+ int defaultIndex,
2002
+ ContextFlatDispatchEntry<T>[] target,
2003
+ int writeIndex
2322
2004
  )
2323
2005
  where T : IMessage
2324
2006
  {
2325
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2326
- return typedHandler.GetOrCreateBroadcastPostLink();
2327
- }
2007
+ if (!GetHandlerForType(messageBus, out TypedHandler<T> typedHandler))
2008
+ {
2009
+ return writeIndex;
2010
+ }
2328
2011
 
2329
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2330
- internal BroadcastWithoutSourceDispatchLink<T> GetOrCreateBroadcastWithoutSourceDispatchLink<T>(
2331
- IMessageBus messageBus
2332
- )
2333
- where T : IMessage
2334
- {
2335
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2336
- return typedHandler.GetOrCreateBroadcastWithoutSourceLink();
2012
+ writeIndex = FillFastWithContextFlatEntries(
2013
+ typedHandler.GetPriorityHandlers(fastIndex),
2014
+ priority,
2015
+ target,
2016
+ writeIndex
2017
+ );
2018
+ return FillDefaultWithContextFlatEntries(
2019
+ typedHandler.GetPriorityHandlers(defaultIndex),
2020
+ priority,
2021
+ target,
2022
+ writeIndex
2023
+ );
2337
2024
  }
2338
2025
 
2339
2026
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
2340
- internal BroadcastWithoutSourcePostDispatchLink<T> GetOrCreateBroadcastWithoutSourcePostDispatchLink<T>(
2341
- IMessageBus messageBus
2027
+ private static Dictionary<int, IHandlerActionCache> GetContextPriorityHandlers<T>(
2028
+ TypedHandler<T> typedHandler,
2029
+ int slotIndex,
2030
+ InstanceId context
2342
2031
  )
2343
2032
  where T : IMessage
2344
2033
  {
2345
- TypedHandler<T> typedHandler = GetOrCreateHandlerForType<T>(messageBus);
2346
- return typedHandler.GetOrCreateBroadcastWithoutSourcePostLink();
2347
- }
2348
-
2349
- internal sealed class HandlerActionCache<T> : DxMessaging.Core.Internal.IHandlerActionCache
2350
- {
2351
- // Uses outer T as a field type -- reflection callers must close
2352
- // via MakeGenericType(outer.GetGenericArguments()) before passing
2353
- // this type to Activator.CreateInstance. See
2354
- // Tests/Editor/Contract/ReflectionHelpers.cs::CloseNestedGeneric.
2355
- internal readonly struct Entry
2356
- {
2357
- /// <summary>
2358
- /// Initializes an entry used to track handler invocation counts.
2359
- /// </summary>
2360
- /// <param name="handler">Handler delegate being tracked.</param>
2361
- /// <param name="count">Number of times the handler has been cached.</param>
2362
- public Entry(T handler, int count)
2363
- {
2364
- this.handler = handler;
2365
- this.count = count;
2366
- }
2367
-
2368
- public readonly T handler;
2369
- public readonly int count;
2370
- }
2371
-
2372
- public readonly Dictionary<T, Entry> entries = new();
2373
- public readonly List<T> cache = new();
2374
- public long version;
2375
- public long lastSeenVersion = -1;
2376
- public long lastSeenEmissionId = -1;
2377
- internal int prefreezeInvocationCount;
2378
-
2379
- /// <summary>Monotonic version field, read-only on the interface surface.</summary>
2380
- long DxMessaging.Core.Internal.IHandlerActionCache.Version
2381
- {
2382
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2383
- get => version;
2384
- }
2385
-
2386
- /// <summary>Most recent dispatcher-observed version; mutable through the staged dispatch path.</summary>
2387
- long DxMessaging.Core.Internal.IHandlerActionCache.LastSeenVersion
2388
- {
2389
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2390
- get => lastSeenVersion;
2391
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2392
- set => lastSeenVersion = value;
2393
- }
2394
-
2395
- /// <summary>Most recent dispatcher-observed bus emission id.</summary>
2396
- long DxMessaging.Core.Internal.IHandlerActionCache.LastSeenEmissionId
2397
- {
2398
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2399
- get => lastSeenEmissionId;
2400
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2401
- set => lastSeenEmissionId = value;
2402
- }
2403
-
2404
- /// <summary>Prefreeze invocation counter mirror; maintained by the dispatchers.</summary>
2405
- int DxMessaging.Core.Internal.IHandlerActionCache.PrefreezeInvocationCount
2406
- {
2407
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2408
- get => prefreezeInvocationCount;
2409
- }
2410
-
2411
- /// <summary>True iff the entries dictionary holds zero handlers.</summary>
2412
- bool DxMessaging.Core.Internal.IHandlerActionCache.IsEmpty
2034
+ Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> byContext =
2035
+ typedHandler.GetContextHandlers(slotIndex);
2036
+ if (
2037
+ byContext != null
2038
+ && byContext.TryGetValue(
2039
+ context,
2040
+ out Dictionary<int, IHandlerActionCache> byPriority
2041
+ )
2042
+ )
2413
2043
  {
2414
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2415
- get => entries.Count == 0;
2044
+ return byPriority;
2416
2045
  }
2417
2046
 
2418
- /// <summary>
2419
- /// Eviction-driven full clear; bumps <see cref="version"/> as the LAST step
2420
- /// so captured dispatch closures observe invalidation.
2421
- /// </summary>
2422
- void DxMessaging.Core.Internal.IHandlerActionCache.Reset()
2423
- {
2424
- entries.Clear();
2425
- cache.Clear();
2426
- lastSeenVersion = -1;
2427
- lastSeenEmissionId = -1;
2428
- prefreezeInvocationCount = 0;
2429
- unchecked
2430
- {
2431
- ++version;
2432
- }
2433
- }
2047
+ return null;
2434
2048
  }
2435
2049
 
2436
- internal sealed class UntargetedDispatchLink<T>
2437
- where T : IMessage
2050
+ private static int CountFlatDelegates<TDelegate>(
2051
+ Dictionary<int, IHandlerActionCache> byPriority,
2052
+ int priority
2053
+ )
2438
2054
  {
2439
- private readonly TypedHandler<T> typedHandler;
2440
- internal readonly long capturedGeneration;
2441
-
2442
- internal UntargetedDispatchLink(TypedHandler<T> typedHandler, long capturedGeneration)
2443
- {
2444
- this.typedHandler = typedHandler;
2445
- this.capturedGeneration = capturedGeneration;
2446
- }
2447
-
2448
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2449
- internal void Invoke(
2450
- MessageHandler messageHandler,
2451
- ref T message,
2452
- int priority,
2453
- long emissionId
2055
+ if (
2056
+ byPriority != null
2057
+ && byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2058
+ && erased is HandlerActionCache<TDelegate> cache
2454
2059
  )
2455
2060
  {
2456
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2457
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2458
- // walks when the outer wrapper has been reclaimed.
2459
- if (typedHandler._outerGeneration != capturedGeneration)
2460
- {
2461
- return;
2462
- }
2463
-
2464
- if (!messageHandler.active)
2465
- {
2466
- return;
2467
- }
2468
-
2469
- typedHandler.HandleUntargeted(ref message, priority, emissionId);
2061
+ return cache.insertionOrder.Count;
2470
2062
  }
2063
+
2064
+ return 0;
2471
2065
  }
2472
2066
 
2473
- internal sealed class UntargetedPostDispatchLink<TMessage>
2474
- where TMessage : IMessage
2067
+ private int FillFastFlatEntries<T>(
2068
+ Dictionary<int, IHandlerActionCache> byPriority,
2069
+ int priority,
2070
+ FlatDispatchEntry<T>[] target,
2071
+ int writeIndex
2072
+ )
2073
+ where T : IMessage
2475
2074
  {
2476
- private readonly TypedHandler<TMessage> typedHandler;
2477
- internal readonly long capturedGeneration;
2478
-
2479
- internal UntargetedPostDispatchLink(
2480
- TypedHandler<TMessage> typedHandler,
2481
- long capturedGeneration
2075
+ if (
2076
+ byPriority == null
2077
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2078
+ || erased is not HandlerActionCache<FastHandler<T>> cache
2482
2079
  )
2483
2080
  {
2484
- this.typedHandler = typedHandler;
2485
- this.capturedGeneration = capturedGeneration;
2081
+ return writeIndex;
2486
2082
  }
2487
2083
 
2488
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2489
- internal void Invoke(
2490
- MessageHandler messageHandler,
2491
- ref TMessage message,
2492
- int priority,
2493
- long emissionId
2494
- )
2084
+ List<FastHandler<T>> ordered = cache.insertionOrder;
2085
+ int orderedCount = ordered.Count;
2086
+ for (int i = 0; i < orderedCount; ++i)
2495
2087
  {
2496
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2497
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2498
- // walks when the outer wrapper has been reclaimed.
2499
- if (typedHandler._outerGeneration != capturedGeneration)
2500
- {
2501
- return;
2502
- }
2503
-
2504
- if (!messageHandler.active)
2088
+ if (
2089
+ cache.entries.TryGetValue(
2090
+ ordered[i],
2091
+ out HandlerActionCache<FastHandler<T>>.Entry entry
2092
+ )
2093
+ )
2505
2094
  {
2506
- return;
2095
+ target[writeIndex++] = new FlatDispatchEntry<T>(this, entry.handler);
2507
2096
  }
2508
-
2509
- typedHandler.HandleUntargetedPostProcessing(ref message, priority, emissionId);
2510
2097
  }
2098
+
2099
+ return writeIndex;
2511
2100
  }
2512
2101
 
2513
- internal sealed class TargetedDispatchLink<TMessage>
2514
- where TMessage : IMessage
2102
+ private int FillDefaultFlatEntries<T>(
2103
+ Dictionary<int, IHandlerActionCache> byPriority,
2104
+ int priority,
2105
+ FlatDispatchEntry<T>[] target,
2106
+ int writeIndex
2107
+ )
2108
+ where T : IMessage
2515
2109
  {
2516
- private readonly TypedHandler<TMessage> typedHandler;
2517
- internal readonly long capturedGeneration;
2518
-
2519
- internal TargetedDispatchLink(
2520
- TypedHandler<TMessage> typedHandler,
2521
- long capturedGeneration
2110
+ if (
2111
+ byPriority == null
2112
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2113
+ || erased is not HandlerActionCache<Action<T>> cache
2522
2114
  )
2523
2115
  {
2524
- this.typedHandler = typedHandler;
2525
- this.capturedGeneration = capturedGeneration;
2116
+ return writeIndex;
2526
2117
  }
2527
2118
 
2528
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2529
- internal void Invoke(
2530
- MessageHandler messageHandler,
2531
- ref InstanceId target,
2532
- ref TMessage message,
2533
- int priority,
2534
- long emissionId
2535
- )
2119
+ List<Action<T>> ordered = cache.insertionOrder;
2120
+ int orderedCount = ordered.Count;
2121
+ for (int i = 0; i < orderedCount; ++i)
2536
2122
  {
2537
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2538
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2539
- // walks when the outer wrapper has been reclaimed.
2540
- if (typedHandler._outerGeneration != capturedGeneration)
2123
+ if (
2124
+ !cache.entries.TryGetValue(
2125
+ ordered[i],
2126
+ out HandlerActionCache<Action<T>>.Entry entry
2127
+ )
2128
+ )
2541
2129
  {
2542
- return;
2130
+ continue;
2543
2131
  }
2544
2132
 
2545
- typedHandler.HandleTargeted(ref target, ref message, priority, emissionId);
2546
- }
2547
- }
2548
-
2549
- internal sealed class TargetedPostDispatchLink<TMessage>
2550
- where TMessage : IMessage
2551
- {
2552
- private readonly TypedHandler<TMessage> typedHandler;
2553
- internal readonly long capturedGeneration;
2554
-
2555
- internal TargetedPostDispatchLink(
2556
- TypedHandler<TMessage> typedHandler,
2557
- long capturedGeneration
2558
- )
2559
- {
2560
- this.typedHandler = typedHandler;
2561
- this.capturedGeneration = capturedGeneration;
2562
- }
2563
-
2564
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2565
- internal void Invoke(
2566
- MessageHandler messageHandler,
2567
- ref InstanceId target,
2568
- ref TMessage message,
2569
- int priority,
2570
- long emissionId
2571
- )
2572
- {
2573
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2574
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2575
- // walks when the outer wrapper has been reclaimed.
2576
- if (typedHandler._outerGeneration != capturedGeneration)
2133
+ // Every default registration path for the flattened slots
2134
+ // supplies the adapter at registration time (AddUntargetedHandler,
2135
+ // AddTargetedHandler, AddSourcedBroadcastHandler, and their
2136
+ // post-processor siblings). The type test doubles as a null
2137
+ // guard; a missing adapter would indicate a new registration
2138
+ // path that forgot to provide one.
2139
+ if (entry.flatInvoker is FastHandler<T> invoker)
2577
2140
  {
2578
- return;
2141
+ target[writeIndex++] = new FlatDispatchEntry<T>(this, invoker);
2579
2142
  }
2580
-
2581
- typedHandler.HandleTargetedPostProcessing(
2582
- ref target,
2583
- ref message,
2584
- priority,
2585
- emissionId
2586
- );
2587
- }
2588
- }
2589
-
2590
- internal sealed class TargetedWithoutTargetingDispatchLink<TMessage>
2591
- where TMessage : IMessage
2592
- {
2593
- private readonly TypedHandler<TMessage> typedHandler;
2594
- internal readonly long capturedGeneration;
2595
-
2596
- internal TargetedWithoutTargetingDispatchLink(
2597
- TypedHandler<TMessage> typedHandler,
2598
- long capturedGeneration
2599
- )
2600
- {
2601
- this.typedHandler = typedHandler;
2602
- this.capturedGeneration = capturedGeneration;
2603
- }
2604
-
2605
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2606
- internal void Invoke(
2607
- MessageHandler messageHandler,
2608
- ref InstanceId target,
2609
- ref TMessage message,
2610
- int priority,
2611
- long emissionId
2612
- )
2613
- {
2614
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2615
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2616
- // walks when the outer wrapper has been reclaimed.
2617
- if (typedHandler._outerGeneration != capturedGeneration)
2143
+ else
2618
2144
  {
2619
- return;
2145
+ System.Diagnostics.Debug.Assert(
2146
+ false,
2147
+ "Default registration is missing its FastHandler flat invoker; "
2148
+ + "every Add*Handler/Add*PostProcessor default path must adapt "
2149
+ + "the augmented handler at registration time."
2150
+ );
2620
2151
  }
2621
-
2622
- typedHandler.HandleTargetedWithoutTargeting(
2623
- ref target,
2624
- ref message,
2625
- priority,
2626
- emissionId
2627
- );
2628
2152
  }
2153
+
2154
+ return writeIndex;
2629
2155
  }
2630
2156
 
2631
- internal sealed class TargetedWithoutTargetingPostDispatchLink<TMessage>
2632
- where TMessage : IMessage
2157
+ private int FillFastWithContextFlatEntries<T>(
2158
+ Dictionary<int, IHandlerActionCache> byPriority,
2159
+ int priority,
2160
+ ContextFlatDispatchEntry<T>[] target,
2161
+ int writeIndex
2162
+ )
2163
+ where T : IMessage
2633
2164
  {
2634
- private readonly TypedHandler<TMessage> typedHandler;
2635
- internal readonly long capturedGeneration;
2636
-
2637
- internal TargetedWithoutTargetingPostDispatchLink(
2638
- TypedHandler<TMessage> typedHandler,
2639
- long capturedGeneration
2165
+ if (
2166
+ byPriority == null
2167
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2168
+ || erased is not HandlerActionCache<FastHandlerWithContext<T>> cache
2640
2169
  )
2641
2170
  {
2642
- this.typedHandler = typedHandler;
2643
- this.capturedGeneration = capturedGeneration;
2171
+ return writeIndex;
2644
2172
  }
2645
2173
 
2646
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2647
- internal void Invoke(
2648
- MessageHandler messageHandler,
2649
- ref InstanceId target,
2650
- ref TMessage message,
2651
- int priority,
2652
- long emissionId
2653
- )
2174
+ List<FastHandlerWithContext<T>> ordered = cache.insertionOrder;
2175
+ int orderedCount = ordered.Count;
2176
+ for (int i = 0; i < orderedCount; ++i)
2654
2177
  {
2655
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2656
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2657
- // walks when the outer wrapper has been reclaimed.
2658
- if (typedHandler._outerGeneration != capturedGeneration)
2178
+ if (
2179
+ cache.entries.TryGetValue(
2180
+ ordered[i],
2181
+ out HandlerActionCache<FastHandlerWithContext<T>>.Entry entry
2182
+ )
2183
+ )
2659
2184
  {
2660
- return;
2185
+ target[writeIndex++] = new ContextFlatDispatchEntry<T>(this, entry.handler);
2661
2186
  }
2662
-
2663
- typedHandler.HandleTargetedWithoutTargetingPostProcessing(
2664
- ref target,
2665
- ref message,
2666
- priority,
2667
- emissionId
2668
- );
2669
2187
  }
2188
+
2189
+ return writeIndex;
2670
2190
  }
2671
2191
 
2672
- internal sealed class BroadcastDispatchLink<TMessage>
2673
- where TMessage : IMessage
2192
+ private int FillDefaultWithContextFlatEntries<T>(
2193
+ Dictionary<int, IHandlerActionCache> byPriority,
2194
+ int priority,
2195
+ ContextFlatDispatchEntry<T>[] target,
2196
+ int writeIndex
2197
+ )
2198
+ where T : IMessage
2674
2199
  {
2675
- private readonly TypedHandler<TMessage> typedHandler;
2676
- internal readonly long capturedGeneration;
2677
-
2678
- internal BroadcastDispatchLink(
2679
- TypedHandler<TMessage> typedHandler,
2680
- long capturedGeneration
2200
+ if (
2201
+ byPriority == null
2202
+ || !byPriority.TryGetValue(priority, out IHandlerActionCache erased)
2203
+ || erased is not HandlerActionCache<Action<InstanceId, T>> cache
2681
2204
  )
2682
2205
  {
2683
- this.typedHandler = typedHandler;
2684
- this.capturedGeneration = capturedGeneration;
2206
+ return writeIndex;
2685
2207
  }
2686
2208
 
2687
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2688
- internal void Invoke(
2689
- MessageHandler messageHandler,
2690
- ref InstanceId source,
2691
- ref TMessage message,
2692
- int priority,
2693
- long emissionId
2694
- )
2209
+ List<Action<InstanceId, T>> ordered = cache.insertionOrder;
2210
+ int orderedCount = ordered.Count;
2211
+ for (int i = 0; i < orderedCount; ++i)
2695
2212
  {
2696
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2697
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2698
- // walks when the outer wrapper has been reclaimed.
2699
- if (typedHandler._outerGeneration != capturedGeneration)
2213
+ if (
2214
+ !cache.entries.TryGetValue(
2215
+ ordered[i],
2216
+ out HandlerActionCache<Action<InstanceId, T>>.Entry entry
2217
+ )
2218
+ )
2700
2219
  {
2701
- return;
2220
+ continue;
2702
2221
  }
2703
2222
 
2704
- typedHandler.HandleSourcedBroadcast(ref source, ref message, priority, emissionId);
2223
+ // See FillDefaultFlatEntries: the adapter is created once at
2224
+ // registration time (AddTargetedWithoutTargetingHandler,
2225
+ // AddSourcedBroadcastWithoutSourceHandler, and their
2226
+ // post-processor siblings).
2227
+ if (entry.flatInvoker is FastHandlerWithContext<T> invoker)
2228
+ {
2229
+ target[writeIndex++] = new ContextFlatDispatchEntry<T>(this, invoker);
2230
+ }
2231
+ else
2232
+ {
2233
+ System.Diagnostics.Debug.Assert(
2234
+ false,
2235
+ "Default with-context registration is missing its "
2236
+ + "FastHandlerWithContext flat invoker; every without-context "
2237
+ + "Add* default path must adapt the augmented handler at "
2238
+ + "registration time."
2239
+ );
2240
+ }
2705
2241
  }
2242
+
2243
+ return writeIndex;
2706
2244
  }
2707
2245
 
2708
- internal sealed class BroadcastPostDispatchLink<TMessage>
2709
- where TMessage : IMessage
2246
+ internal sealed class HandlerActionCache<T> : DxMessaging.Core.Internal.IHandlerActionCache
2710
2247
  {
2711
- private readonly TypedHandler<TMessage> typedHandler;
2712
- internal readonly long capturedGeneration;
2713
-
2714
- internal BroadcastPostDispatchLink(
2715
- TypedHandler<TMessage> typedHandler,
2716
- long capturedGeneration
2717
- )
2248
+ // Uses outer T as a field type -- reflection callers must close
2249
+ // via MakeGenericType(outer.GetGenericArguments()) before passing
2250
+ // this type to Activator.CreateInstance. See
2251
+ // Tests/Editor/Contract/ReflectionHelpers.cs::CloseNestedGeneric.
2252
+ internal readonly struct Entry
2718
2253
  {
2719
- this.typedHandler = typedHandler;
2720
- this.capturedGeneration = capturedGeneration;
2721
- }
2254
+ /// <summary>
2255
+ /// Initializes an entry used to track handler invocation counts.
2256
+ /// </summary>
2257
+ /// <param name="handler">Handler delegate being tracked.</param>
2258
+ /// <param name="count">Number of times the handler has been cached.</param>
2259
+ public Entry(T handler, int count)
2260
+ : this(handler, count, null) { }
2722
2261
 
2723
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2724
- internal void Invoke(
2725
- MessageHandler messageHandler,
2726
- ref InstanceId source,
2727
- ref TMessage message,
2728
- int priority,
2729
- long emissionId
2730
- )
2731
- {
2732
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2733
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2734
- // walks when the outer wrapper has been reclaimed.
2735
- if (typedHandler._outerGeneration != capturedGeneration)
2262
+ /// <summary>
2263
+ /// Initializes an entry that additionally carries a pre-resolved
2264
+ /// flat-dispatch invoker (see <see cref="flatInvoker"/>).
2265
+ /// </summary>
2266
+ /// <param name="handler">Handler delegate being tracked.</param>
2267
+ /// <param name="count">Number of times the handler has been cached.</param>
2268
+ /// <param name="flatInvoker">Pre-resolved flat-dispatch invoker, if any.</param>
2269
+ public Entry(T handler, int count, object flatInvoker)
2736
2270
  {
2737
- return;
2271
+ this.handler = handler;
2272
+ this.count = count;
2273
+ this.flatInvoker = flatInvoker;
2738
2274
  }
2739
2275
 
2740
- typedHandler.HandleSourcedBroadcastPostProcessing(
2741
- ref source,
2742
- ref message,
2743
- priority,
2744
- emissionId
2745
- );
2276
+ public readonly T handler;
2277
+ public readonly int count;
2278
+
2279
+ // Pre-resolved invoker consumed by the bus-side flat dispatch
2280
+ // snapshot (MessageBus.BuildMessageFlatDispatch). For
2281
+ // default Action<TMessage> registrations this holds a
2282
+ // FastHandler<TMessage> adapter wrapping the AUGMENTED
2283
+ // handler, created exactly ONCE at registration time so
2284
+ // snapshot rebuilds never allocate closures. For delegate
2285
+ // shapes the flat path does not consume (fast handlers, which
2286
+ // already ARE the invoker, and every targeted/broadcast
2287
+ // shape) this stays null. Refcount increments and decrements
2288
+ // preserve the first registration's invoker, mirroring the
2289
+ // first-augmented-handler-wins semantics of `handler`.
2290
+ public readonly object flatInvoker;
2746
2291
  }
2747
- }
2748
2292
 
2749
- internal sealed class BroadcastWithoutSourceDispatchLink<TMessage>
2750
- where TMessage : IMessage
2751
- {
2752
- private readonly TypedHandler<TMessage> typedHandler;
2753
- internal readonly long capturedGeneration;
2293
+ public readonly Dictionary<T, Entry> entries = new();
2754
2294
 
2755
- internal BroadcastWithoutSourceDispatchLink(
2756
- TypedHandler<TMessage> typedHandler,
2757
- long capturedGeneration
2758
- )
2295
+ // Original-handler keys in first-registration order. Dictionary
2296
+ // enumeration order is NOT stable across Remove/Add churn (.NET
2297
+ // reuses freed slots LIFO), so dispatch snapshots are rebuilt from
2298
+ // this list instead of from <see cref="entries"/> to honor the
2299
+ // documented "same priority uses registration order" contract.
2300
+ // Invariants: contains exactly the keys of <see cref="entries"/>;
2301
+ // a key is appended on its FIRST registration only (refcount
2302
+ // increments do not move it) and removed when its refcount drops
2303
+ // to zero. Maintained exclusively by the AddHandler* family and
2304
+ // <see cref="DxMessaging.Core.Internal.IHandlerActionCache.Reset"/>.
2305
+ public readonly List<T> insertionOrder = new();
2306
+ public readonly List<T> cache = new();
2307
+ public long version;
2308
+ public long lastSeenVersion = -1;
2309
+ public long lastSeenEmissionId = -1;
2310
+
2311
+ /// <summary>Monotonic version field, read-only on the interface surface.</summary>
2312
+ long DxMessaging.Core.Internal.IHandlerActionCache.Version
2759
2313
  {
2760
- this.typedHandler = typedHandler;
2761
- this.capturedGeneration = capturedGeneration;
2314
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2315
+ get => version;
2762
2316
  }
2763
2317
 
2764
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2765
- internal void Invoke(
2766
- MessageHandler messageHandler,
2767
- ref InstanceId source,
2768
- ref TMessage message,
2769
- int priority,
2770
- long emissionId
2771
- )
2318
+ /// <summary>Most recent dispatcher-observed version; mutable through the staged dispatch path.</summary>
2319
+ long DxMessaging.Core.Internal.IHandlerActionCache.LastSeenVersion
2772
2320
  {
2773
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2774
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2775
- // walks when the outer wrapper has been reclaimed.
2776
- if (typedHandler._outerGeneration != capturedGeneration)
2777
- {
2778
- return;
2779
- }
2780
-
2781
- typedHandler.HandleSourcedBroadcastWithoutSource(
2782
- ref source,
2783
- ref message,
2784
- priority,
2785
- emissionId
2786
- );
2321
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2322
+ get => lastSeenVersion;
2323
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2324
+ set => lastSeenVersion = value;
2787
2325
  }
2788
- }
2789
2326
 
2790
- internal sealed class BroadcastWithoutSourcePostDispatchLink<TMessage>
2791
- where TMessage : IMessage
2792
- {
2793
- private readonly TypedHandler<TMessage> typedHandler;
2794
- internal readonly long capturedGeneration;
2327
+ /// <summary>Most recent dispatcher-observed bus emission id.</summary>
2328
+ long DxMessaging.Core.Internal.IHandlerActionCache.LastSeenEmissionId
2329
+ {
2330
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2331
+ get => lastSeenEmissionId;
2332
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2333
+ set => lastSeenEmissionId = value;
2334
+ }
2795
2335
 
2796
- internal BroadcastWithoutSourcePostDispatchLink(
2797
- TypedHandler<TMessage> typedHandler,
2798
- long capturedGeneration
2799
- )
2336
+ /// <summary>True iff the entries dictionary holds zero handlers.</summary>
2337
+ bool DxMessaging.Core.Internal.IHandlerActionCache.IsEmpty
2800
2338
  {
2801
- this.typedHandler = typedHandler;
2802
- this.capturedGeneration = capturedGeneration;
2339
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
2340
+ get => entries.Count == 0;
2803
2341
  }
2804
2342
 
2805
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
2806
- internal void Invoke(
2807
- MessageHandler messageHandler,
2808
- ref InstanceId source,
2809
- ref TMessage message,
2810
- int priority,
2811
- long emissionId
2812
- )
2343
+ /// <summary>
2344
+ /// Eviction-driven full clear; bumps <see cref="version"/> as the LAST step
2345
+ /// so captured dispatch closures observe invalidation.
2346
+ /// </summary>
2347
+ void DxMessaging.Core.Internal.IHandlerActionCache.Reset()
2813
2348
  {
2814
- // Generation guard: 1 field read + 1 compare per dispatch on the hot path.
2815
- // Sits at the top of Invoke so reclaimed wrappers return before handler-slot
2816
- // walks when the outer wrapper has been reclaimed.
2817
- if (typedHandler._outerGeneration != capturedGeneration)
2349
+ entries.Clear();
2350
+ insertionOrder.Clear();
2351
+ cache.Clear();
2352
+ lastSeenVersion = -1;
2353
+ lastSeenEmissionId = -1;
2354
+ unchecked
2818
2355
  {
2819
- return;
2356
+ ++version;
2820
2357
  }
2821
-
2822
- typedHandler.HandleBroadcastWithoutSourcePostProcessing(
2823
- ref source,
2824
- ref message,
2825
- priority,
2826
- emissionId
2827
- );
2828
2358
  }
2829
2359
  }
2830
2360
 
@@ -2835,14 +2365,13 @@ namespace DxMessaging.Core
2835
2365
  internal sealed class TypedHandler<T> : ITypedHandlerSlotSweeper
2836
2366
  where T : IMessage
2837
2367
  {
2838
- // Typed storage: 20 typed slots + 6 global slots + 10 dispatch
2839
- // links. The legacy named fields were deleted so new handler
2840
- // variants must pick an explicit axis-indexed slot.
2368
+ // Typed storage: 20 typed slots + 6 global slots. The legacy
2369
+ // named fields were deleted so new handler variants must pick an
2370
+ // explicit axis-indexed slot.
2841
2371
  internal readonly TypedSlot<T>[] _slots = new TypedSlot<T>[TypedSlotIndex.Length];
2842
2372
  internal readonly TypedGlobalSlot[] _globalSlots = new TypedGlobalSlot[
2843
2373
  TypedGlobalSlotIndex.Length
2844
2374
  ];
2845
- internal readonly object[] _dispatchLinks = new object[TypedDispatchLinkIndex.Length];
2846
2375
 
2847
2376
  // Constructor exists solely so the [Conditional("DEBUG")]
2848
2377
  // validator below runs at construction time. In Release builds
@@ -2855,7 +2384,6 @@ namespace DxMessaging.Core
2855
2384
  ValidateSlotArrays();
2856
2385
  }
2857
2386
 
2858
- internal long _outerGeneration;
2859
2387
  internal bool _markedForOuterRemoval;
2860
2388
 
2861
2389
  int ITypedHandlerSlotSweeper.MessageTypeIndex
@@ -2885,12 +2413,6 @@ namespace DxMessaging.Core
2885
2413
  $"_globalSlots length is {_globalSlots.Length} but TypedGlobalSlotIndex.Length is {TypedGlobalSlotIndex.Length}."
2886
2414
  );
2887
2415
  }
2888
- if (_dispatchLinks.Length != TypedDispatchLinkIndex.Length)
2889
- {
2890
- throw new InvalidOperationException(
2891
- $"_dispatchLinks length is {_dispatchLinks.Length} but TypedDispatchLinkIndex.Length is {TypedDispatchLinkIndex.Length}."
2892
- );
2893
- }
2894
2416
  // Lazy registration writers update the slot arrays; this assertion still
2895
2417
  // holds at construction (slots populate on first register,
2896
2418
  // not on construction). The invariant flips meaning -- not
@@ -2913,15 +2435,6 @@ namespace DxMessaging.Core
2913
2435
  );
2914
2436
  }
2915
2437
  }
2916
- for (int i = 0; i < _dispatchLinks.Length; ++i)
2917
- {
2918
- if (_dispatchLinks[i] != null)
2919
- {
2920
- throw new InvalidOperationException(
2921
- $"_dispatchLinks[{i}] is non-null at construction; expected null per TypedDispatchLinkIndex because links populate lazily on first dispatch-link request."
2922
- );
2923
- }
2924
- }
2925
2438
  }
2926
2439
 
2927
2440
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -3079,11 +2592,6 @@ namespace DxMessaging.Core
3079
2592
  }
3080
2593
  }
3081
2594
 
3082
- ClearDispatchLinks();
3083
- unchecked
3084
- {
3085
- ++_outerGeneration;
3086
- }
3087
2595
  return resetCount;
3088
2596
  }
3089
2597
 
@@ -3119,12 +2627,7 @@ namespace DxMessaging.Core
3119
2627
  return;
3120
2628
  }
3121
2629
 
3122
- ClearDispatchLinks();
3123
2630
  _markedForOuterRemoval = true;
3124
- unchecked
3125
- {
3126
- ++_outerGeneration;
3127
- }
3128
2631
  }
3129
2632
 
3130
2633
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -3149,166 +2652,6 @@ namespace DxMessaging.Core
3149
2652
  return false;
3150
2653
  }
3151
2654
 
3152
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3153
- private void ClearDispatchLinks()
3154
- {
3155
- for (int i = 0; i < _dispatchLinks.Length; ++i)
3156
- {
3157
- _dispatchLinks[i] = null;
3158
- }
3159
- }
3160
-
3161
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3162
- internal UntargetedDispatchLink<T> GetOrCreateUntargetedLink()
3163
- {
3164
- UntargetedDispatchLink<T> link =
3165
- _dispatchLinks[TypedDispatchLinkIndex.UntargetedHandle]
3166
- as UntargetedDispatchLink<T>;
3167
- if (link == null)
3168
- {
3169
- link = new UntargetedDispatchLink<T>(this, _outerGeneration);
3170
- _dispatchLinks[TypedDispatchLinkIndex.UntargetedHandle] = link;
3171
- }
3172
-
3173
- return link;
3174
- }
3175
-
3176
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3177
- internal UntargetedPostDispatchLink<T> GetOrCreateUntargetedPostLink()
3178
- {
3179
- UntargetedPostDispatchLink<T> link =
3180
- _dispatchLinks[TypedDispatchLinkIndex.UntargetedPostProcess]
3181
- as UntargetedPostDispatchLink<T>;
3182
- if (link == null)
3183
- {
3184
- link = new UntargetedPostDispatchLink<T>(this, _outerGeneration);
3185
- _dispatchLinks[TypedDispatchLinkIndex.UntargetedPostProcess] = link;
3186
- }
3187
-
3188
- return link;
3189
- }
3190
-
3191
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3192
- internal TargetedDispatchLink<T> GetOrCreateTargetedLink()
3193
- {
3194
- TargetedDispatchLink<T> link =
3195
- _dispatchLinks[TypedDispatchLinkIndex.TargetedHandle]
3196
- as TargetedDispatchLink<T>;
3197
- if (link == null)
3198
- {
3199
- link = new TargetedDispatchLink<T>(this, _outerGeneration);
3200
- _dispatchLinks[TypedDispatchLinkIndex.TargetedHandle] = link;
3201
- }
3202
-
3203
- return link;
3204
- }
3205
-
3206
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3207
- internal TargetedPostDispatchLink<T> GetOrCreateTargetedPostLink()
3208
- {
3209
- TargetedPostDispatchLink<T> link =
3210
- _dispatchLinks[TypedDispatchLinkIndex.TargetedPostProcess]
3211
- as TargetedPostDispatchLink<T>;
3212
- if (link == null)
3213
- {
3214
- link = new TargetedPostDispatchLink<T>(this, _outerGeneration);
3215
- _dispatchLinks[TypedDispatchLinkIndex.TargetedPostProcess] = link;
3216
- }
3217
-
3218
- return link;
3219
- }
3220
-
3221
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3222
- internal TargetedWithoutTargetingDispatchLink<T> GetOrCreateTargetedWithoutTargetingLink()
3223
- {
3224
- TargetedWithoutTargetingDispatchLink<T> link =
3225
- _dispatchLinks[TypedDispatchLinkIndex.TargetedHandleWithoutContext]
3226
- as TargetedWithoutTargetingDispatchLink<T>;
3227
- if (link == null)
3228
- {
3229
- link = new TargetedWithoutTargetingDispatchLink<T>(this, _outerGeneration);
3230
- _dispatchLinks[TypedDispatchLinkIndex.TargetedHandleWithoutContext] = link;
3231
- }
3232
-
3233
- return link;
3234
- }
3235
-
3236
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3237
- internal TargetedWithoutTargetingPostDispatchLink<T> GetOrCreateTargetedWithoutTargetingPostLink()
3238
- {
3239
- TargetedWithoutTargetingPostDispatchLink<T> link =
3240
- _dispatchLinks[TypedDispatchLinkIndex.TargetedPostProcessWithoutContext]
3241
- as TargetedWithoutTargetingPostDispatchLink<T>;
3242
- if (link == null)
3243
- {
3244
- link = new TargetedWithoutTargetingPostDispatchLink<T>(this, _outerGeneration);
3245
- _dispatchLinks[TypedDispatchLinkIndex.TargetedPostProcessWithoutContext] = link;
3246
- }
3247
-
3248
- return link;
3249
- }
3250
-
3251
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3252
- internal BroadcastDispatchLink<T> GetOrCreateBroadcastLink()
3253
- {
3254
- BroadcastDispatchLink<T> link =
3255
- _dispatchLinks[TypedDispatchLinkIndex.BroadcastHandle]
3256
- as BroadcastDispatchLink<T>;
3257
- if (link == null)
3258
- {
3259
- link = new BroadcastDispatchLink<T>(this, _outerGeneration);
3260
- _dispatchLinks[TypedDispatchLinkIndex.BroadcastHandle] = link;
3261
- }
3262
-
3263
- return link;
3264
- }
3265
-
3266
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3267
- internal BroadcastPostDispatchLink<T> GetOrCreateBroadcastPostLink()
3268
- {
3269
- BroadcastPostDispatchLink<T> link =
3270
- _dispatchLinks[TypedDispatchLinkIndex.BroadcastPostProcess]
3271
- as BroadcastPostDispatchLink<T>;
3272
- if (link == null)
3273
- {
3274
- link = new BroadcastPostDispatchLink<T>(this, _outerGeneration);
3275
- _dispatchLinks[TypedDispatchLinkIndex.BroadcastPostProcess] = link;
3276
- }
3277
-
3278
- return link;
3279
- }
3280
-
3281
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3282
- internal BroadcastWithoutSourceDispatchLink<T> GetOrCreateBroadcastWithoutSourceLink()
3283
- {
3284
- BroadcastWithoutSourceDispatchLink<T> link =
3285
- _dispatchLinks[TypedDispatchLinkIndex.BroadcastHandleWithoutContext]
3286
- as BroadcastWithoutSourceDispatchLink<T>;
3287
- if (link == null)
3288
- {
3289
- link = new BroadcastWithoutSourceDispatchLink<T>(this, _outerGeneration);
3290
- _dispatchLinks[TypedDispatchLinkIndex.BroadcastHandleWithoutContext] = link;
3291
- }
3292
-
3293
- return link;
3294
- }
3295
-
3296
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
3297
- internal BroadcastWithoutSourcePostDispatchLink<T> GetOrCreateBroadcastWithoutSourcePostLink()
3298
- {
3299
- BroadcastWithoutSourcePostDispatchLink<T> link =
3300
- _dispatchLinks[TypedDispatchLinkIndex.BroadcastPostProcessWithoutContext]
3301
- as BroadcastWithoutSourcePostDispatchLink<T>;
3302
- if (link == null)
3303
- {
3304
- link = new BroadcastWithoutSourcePostDispatchLink<T>(this, _outerGeneration);
3305
- _dispatchLinks[TypedDispatchLinkIndex.BroadcastPostProcessWithoutContext] =
3306
- link;
3307
- }
3308
-
3309
- return link;
3310
- }
3311
-
3312
2655
  /// <summary>
3313
2656
  /// Emits the UntargetedMessage to all subscribed listeners.
3314
2657
  /// </summary>
@@ -3481,7 +2824,7 @@ namespace DxMessaging.Core
3481
2824
  emissionId
3482
2825
  );
3483
2826
  int handlersCount = handlers.Count;
3484
- for (int i = 0; i < handlersCount; ++i)
2827
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
3485
2828
  {
3486
2829
  handlers[i](message);
3487
2830
  }
@@ -3520,7 +2863,7 @@ namespace DxMessaging.Core
3520
2863
  emissionId
3521
2864
  );
3522
2865
  int handlersCount = handlers.Count;
3523
- for (int i = 0; i < handlersCount; ++i)
2866
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
3524
2867
  {
3525
2868
  handlers[i](target, message);
3526
2869
  }
@@ -3569,36 +2912,76 @@ namespace DxMessaging.Core
3569
2912
  case 2:
3570
2913
  {
3571
2914
  handlers[0](source, message);
2915
+ if (handlers.Count < 2)
2916
+ {
2917
+ return;
2918
+ }
3572
2919
  handlers[1](source, message);
3573
2920
  return;
3574
2921
  }
3575
2922
  case 3:
3576
2923
  {
3577
2924
  handlers[0](source, message);
2925
+ if (handlers.Count < 2)
2926
+ {
2927
+ return;
2928
+ }
3578
2929
  handlers[1](source, message);
2930
+ if (handlers.Count < 3)
2931
+ {
2932
+ return;
2933
+ }
3579
2934
  handlers[2](source, message);
3580
2935
  return;
3581
2936
  }
3582
2937
  case 4:
3583
2938
  {
3584
2939
  handlers[0](source, message);
2940
+ if (handlers.Count < 2)
2941
+ {
2942
+ return;
2943
+ }
3585
2944
  handlers[1](source, message);
2945
+ if (handlers.Count < 3)
2946
+ {
2947
+ return;
2948
+ }
3586
2949
  handlers[2](source, message);
2950
+ if (handlers.Count < 4)
2951
+ {
2952
+ return;
2953
+ }
3587
2954
  handlers[3](source, message);
3588
2955
  return;
3589
2956
  }
3590
2957
  case 5:
3591
2958
  {
3592
2959
  handlers[0](source, message);
2960
+ if (handlers.Count < 2)
2961
+ {
2962
+ return;
2963
+ }
3593
2964
  handlers[1](source, message);
2965
+ if (handlers.Count < 3)
2966
+ {
2967
+ return;
2968
+ }
3594
2969
  handlers[2](source, message);
2970
+ if (handlers.Count < 4)
2971
+ {
2972
+ return;
2973
+ }
3595
2974
  handlers[3](source, message);
2975
+ if (handlers.Count < 5)
2976
+ {
2977
+ return;
2978
+ }
3596
2979
  handlers[4](source, message);
3597
2980
  return;
3598
2981
  }
3599
2982
  }
3600
2983
 
3601
- for (int i = 0; i < handlersCount; ++i)
2984
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
3602
2985
  {
3603
2986
  handlers[i](source, message);
3604
2987
  }
@@ -3763,6 +3146,10 @@ namespace DxMessaging.Core
3763
3146
  IMessageBus messageBus
3764
3147
  )
3765
3148
  {
3149
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3150
+ // at registration time, so bus-side flat snapshot rebuilds
3151
+ // resolve default registrations without allocating closures.
3152
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
3766
3153
  return AddHandlerPreservingPriorityKey(
3767
3154
  target,
3768
3155
  GetOrCreateContextHandlers(TypedSlotIndex.TargetedHandleDefault),
@@ -3770,7 +3157,8 @@ namespace DxMessaging.Core
3770
3157
  handler,
3771
3158
  deregistration,
3772
3159
  priority,
3773
- messageBus
3160
+ messageBus,
3161
+ flatInvoker
3774
3162
  );
3775
3163
  }
3776
3164
 
@@ -3817,6 +3205,12 @@ namespace DxMessaging.Core
3817
3205
  IMessageBus messageBus
3818
3206
  )
3819
3207
  {
3208
+ // Adapt the AUGMENTED handler to FastHandlerWithContext form
3209
+ // exactly once, at registration time, so bus-side flat snapshot
3210
+ // rebuilds resolve default registrations without allocating
3211
+ // closures.
3212
+ FastHandlerWithContext<T> flatInvoker = (ref InstanceId context, ref T message) =>
3213
+ handler(context, message);
3820
3214
  return AddHandlerPreservingPriorityKey(
3821
3215
  GetOrCreatePriorityHandlers(
3822
3216
  TypedSlotIndex.TargetedHandleWithoutContext,
@@ -3826,7 +3220,8 @@ namespace DxMessaging.Core
3826
3220
  handler,
3827
3221
  deregistration,
3828
3222
  priority,
3829
- messageBus
3223
+ messageBus,
3224
+ flatInvoker
3830
3225
  );
3831
3226
  }
3832
3227
 
@@ -3873,6 +3268,10 @@ namespace DxMessaging.Core
3873
3268
  IMessageBus messageBus
3874
3269
  )
3875
3270
  {
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);
3876
3275
  return AddHandlerPreservingPriorityKey(
3877
3276
  GetOrCreatePriorityHandlers(
3878
3277
  TypedSlotIndex.UntargetedHandleDefault,
@@ -3882,7 +3281,8 @@ namespace DxMessaging.Core
3882
3281
  handler,
3883
3282
  deregistration,
3884
3283
  priority,
3885
- messageBus
3284
+ messageBus,
3285
+ flatInvoker
3886
3286
  );
3887
3287
  }
3888
3288
 
@@ -3931,6 +3331,10 @@ namespace DxMessaging.Core
3931
3331
  IMessageBus messageBus
3932
3332
  )
3933
3333
  {
3334
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3335
+ // at registration time, so bus-side flat snapshot rebuilds
3336
+ // resolve default registrations without allocating closures.
3337
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
3934
3338
  return AddHandlerPreservingPriorityKey(
3935
3339
  source,
3936
3340
  GetOrCreateContextHandlers(TypedSlotIndex.BroadcastHandleDefault),
@@ -3938,7 +3342,8 @@ namespace DxMessaging.Core
3938
3342
  handler,
3939
3343
  deregistration,
3940
3344
  priority,
3941
- messageBus
3345
+ messageBus,
3346
+ flatInvoker
3942
3347
  );
3943
3348
  }
3944
3349
 
@@ -3985,6 +3390,12 @@ namespace DxMessaging.Core
3985
3390
  IMessageBus messageBus
3986
3391
  )
3987
3392
  {
3393
+ // Adapt the AUGMENTED handler to FastHandlerWithContext form
3394
+ // exactly once, at registration time, so bus-side flat snapshot
3395
+ // rebuilds resolve default registrations without allocating
3396
+ // closures.
3397
+ FastHandlerWithContext<T> flatInvoker = (ref InstanceId context, ref T message) =>
3398
+ handler(context, message);
3988
3399
  // Preserve the priority bucket during the current emission so frozen snapshots remain valid
3989
3400
  return AddHandlerPreservingPriorityKey(
3990
3401
  GetOrCreatePriorityHandlers(
@@ -3995,7 +3406,8 @@ namespace DxMessaging.Core
3995
3406
  handler,
3996
3407
  deregistration,
3997
3408
  priority,
3998
- messageBus
3409
+ messageBus,
3410
+ flatInvoker
3999
3411
  );
4000
3412
  }
4001
3413
 
@@ -4175,6 +3587,10 @@ namespace DxMessaging.Core
4175
3587
  IMessageBus messageBus
4176
3588
  )
4177
3589
  {
3590
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3591
+ // at registration time, so bus-side flat snapshot rebuilds
3592
+ // resolve default registrations without allocating closures.
3593
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
4178
3594
  return AddHandlerPreservingPriorityKey(
4179
3595
  GetOrCreatePriorityHandlers(
4180
3596
  TypedSlotIndex.UntargetedPostProcessDefault,
@@ -4184,7 +3600,8 @@ namespace DxMessaging.Core
4184
3600
  handler,
4185
3601
  deregistration,
4186
3602
  priority,
4187
- messageBus
3603
+ messageBus,
3604
+ flatInvoker
4188
3605
  );
4189
3606
  }
4190
3607
 
@@ -4233,6 +3650,10 @@ namespace DxMessaging.Core
4233
3650
  IMessageBus messageBus
4234
3651
  )
4235
3652
  {
3653
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3654
+ // at registration time, so bus-side flat snapshot rebuilds
3655
+ // resolve default registrations without allocating closures.
3656
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
4236
3657
  return AddHandlerPreservingPriorityKey(
4237
3658
  target,
4238
3659
  GetOrCreateContextHandlers(TypedSlotIndex.TargetedPostProcessDefault),
@@ -4240,7 +3661,8 @@ namespace DxMessaging.Core
4240
3661
  handler,
4241
3662
  deregistration,
4242
3663
  priority,
4243
- messageBus
3664
+ messageBus,
3665
+ flatInvoker
4244
3666
  );
4245
3667
  }
4246
3668
 
@@ -4287,6 +3709,12 @@ namespace DxMessaging.Core
4287
3709
  IMessageBus messageBus
4288
3710
  )
4289
3711
  {
3712
+ // Adapt the AUGMENTED handler to FastHandlerWithContext form
3713
+ // exactly once, at registration time, so bus-side flat snapshot
3714
+ // rebuilds resolve default registrations without allocating
3715
+ // closures.
3716
+ FastHandlerWithContext<T> flatInvoker = (ref InstanceId context, ref T message) =>
3717
+ handler(context, message);
4290
3718
  return AddHandlerPreservingPriorityKey(
4291
3719
  GetOrCreatePriorityHandlers(
4292
3720
  TypedSlotIndex.TargetedPostProcessWithoutContext,
@@ -4296,7 +3724,8 @@ namespace DxMessaging.Core
4296
3724
  handler,
4297
3725
  deregistration,
4298
3726
  priority,
4299
- messageBus
3727
+ messageBus,
3728
+ flatInvoker
4300
3729
  );
4301
3730
  }
4302
3731
 
@@ -4345,6 +3774,10 @@ namespace DxMessaging.Core
4345
3774
  IMessageBus messageBus
4346
3775
  )
4347
3776
  {
3777
+ // Adapt the AUGMENTED handler to FastHandler form exactly once,
3778
+ // at registration time, so bus-side flat snapshot rebuilds
3779
+ // resolve default registrations without allocating closures.
3780
+ FastHandler<T> flatInvoker = (ref T message) => handler(message);
4348
3781
  return AddHandlerPreservingPriorityKey(
4349
3782
  source,
4350
3783
  GetOrCreateContextHandlers(TypedSlotIndex.BroadcastPostProcessDefault),
@@ -4352,7 +3785,8 @@ namespace DxMessaging.Core
4352
3785
  handler,
4353
3786
  deregistration,
4354
3787
  priority,
4355
- messageBus
3788
+ messageBus,
3789
+ flatInvoker
4356
3790
  );
4357
3791
  }
4358
3792
 
@@ -4399,6 +3833,12 @@ namespace DxMessaging.Core
4399
3833
  IMessageBus messageBus
4400
3834
  )
4401
3835
  {
3836
+ // Adapt the AUGMENTED handler to FastHandlerWithContext form
3837
+ // exactly once, at registration time, so bus-side flat snapshot
3838
+ // rebuilds resolve default registrations without allocating
3839
+ // closures.
3840
+ FastHandlerWithContext<T> flatInvoker = (ref InstanceId context, ref T message) =>
3841
+ handler(context, message);
4402
3842
  return AddHandlerPreservingPriorityKey(
4403
3843
  GetOrCreatePriorityHandlers(
4404
3844
  TypedSlotIndex.BroadcastPostProcessWithoutContext,
@@ -4408,7 +3848,8 @@ namespace DxMessaging.Core
4408
3848
  handler,
4409
3849
  deregistration,
4410
3850
  priority,
4411
- messageBus
3851
+ messageBus,
3852
+ flatInvoker
4412
3853
  );
4413
3854
  }
4414
3855
 
@@ -4455,6 +3896,10 @@ namespace DxMessaging.Core
4455
3896
  // MessageHandlers or routing through AddSourcedBroadcastWithoutSourceHandler /
4456
3897
  // AddTargetedWithoutTargetingHandler to avoid the per-(context,priority)
4457
3898
  // outer-dictionary growth.
3899
+ // `flatInvoker` carries the pre-resolved flat-dispatch invoker for
3900
+ // default-shape registrations the bus-side flat snapshot consumes
3901
+ // (FastHandler adapter wrapping the augmented handler); see
3902
+ // HandlerActionCache.Entry.flatInvoker.
4458
3903
  private Action AddHandlerPreservingPriorityKey<TU>(
4459
3904
  InstanceId context,
4460
3905
  Dictionary<InstanceId, Dictionary<int, IHandlerActionCache>> handlersByContext,
@@ -4462,7 +3907,8 @@ namespace DxMessaging.Core
4462
3907
  TU augmentedHandler,
4463
3908
  Action deregistration,
4464
3909
  int priority,
4465
- IMessageBus messageBus
3910
+ IMessageBus messageBus,
3911
+ object flatInvoker = null
4466
3912
  )
4467
3913
  {
4468
3914
  if (
@@ -4497,10 +3943,18 @@ namespace DxMessaging.Core
4497
3943
 
4498
3944
  bool firstRegistration = entry.count == 0;
4499
3945
  entry = firstRegistration
4500
- ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1)
4501
- : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
3946
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1, flatInvoker)
3947
+ : new HandlerActionCache<TU>.Entry(
3948
+ entry.handler,
3949
+ entry.count + 1,
3950
+ entry.flatInvoker
3951
+ );
4502
3952
 
4503
3953
  cache.entries[originalHandler] = entry;
3954
+ if (firstRegistration)
3955
+ {
3956
+ cache.insertionOrder.Add(originalHandler);
3957
+ }
4504
3958
  cache.version++;
4505
3959
  TypedSlot<T> slot = FindContextSlot(handlersByContext);
4506
3960
  if (slot != null)
@@ -4580,6 +4034,15 @@ namespace DxMessaging.Core
4580
4034
  if (localEntry.count <= 1)
4581
4035
  {
4582
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);
4583
4046
  localCache.version++;
4584
4047
  if (localSlot != null)
4585
4048
  {
@@ -4592,7 +4055,8 @@ namespace DxMessaging.Core
4592
4055
 
4593
4056
  localEntry = new HandlerActionCache<TU>.Entry(
4594
4057
  localEntry.handler,
4595
- localEntry.count - 1
4058
+ localEntry.count - 1,
4059
+ localEntry.flatInvoker
4596
4060
  );
4597
4061
 
4598
4062
  localCache.entries[originalHandler] = localEntry;
@@ -4648,6 +4112,10 @@ namespace DxMessaging.Core
4648
4112
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
4649
4113
 
4650
4114
  cache.entries[originalHandler] = entry;
4115
+ if (firstRegistration)
4116
+ {
4117
+ cache.insertionOrder.Add(originalHandler);
4118
+ }
4651
4119
  cache.version++;
4652
4120
 
4653
4121
  Dictionary<
@@ -4686,6 +4154,7 @@ namespace DxMessaging.Core
4686
4154
  if (localEntry.count <= 1)
4687
4155
  {
4688
4156
  _ = localCache.entries.Remove(originalHandler);
4157
+ _ = localCache.insertionOrder.Remove(originalHandler);
4689
4158
  localCache.version++;
4690
4159
  // Deliberately keep the priority and context mappings to preserve
4691
4160
  // frozen snapshots for the current emission.
@@ -4809,7 +4278,7 @@ namespace DxMessaging.Core
4809
4278
  return;
4810
4279
  }
4811
4280
 
4812
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
4281
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
4813
4282
  List<FastHandler<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
4814
4283
  int handlersCount = handlers.Count;
4815
4284
  switch (handlersCount)
@@ -4822,36 +4291,76 @@ namespace DxMessaging.Core
4822
4291
  case 2:
4823
4292
  {
4824
4293
  handlers[0](ref typedMessage);
4294
+ if (handlers.Count < 2)
4295
+ {
4296
+ return;
4297
+ }
4825
4298
  handlers[1](ref typedMessage);
4826
4299
  return;
4827
4300
  }
4828
4301
  case 3:
4829
4302
  {
4830
4303
  handlers[0](ref typedMessage);
4304
+ if (handlers.Count < 2)
4305
+ {
4306
+ return;
4307
+ }
4831
4308
  handlers[1](ref typedMessage);
4309
+ if (handlers.Count < 3)
4310
+ {
4311
+ return;
4312
+ }
4832
4313
  handlers[2](ref typedMessage);
4833
4314
  return;
4834
4315
  }
4835
4316
  case 4:
4836
4317
  {
4837
4318
  handlers[0](ref typedMessage);
4319
+ if (handlers.Count < 2)
4320
+ {
4321
+ return;
4322
+ }
4838
4323
  handlers[1](ref typedMessage);
4324
+ if (handlers.Count < 3)
4325
+ {
4326
+ return;
4327
+ }
4839
4328
  handlers[2](ref typedMessage);
4329
+ if (handlers.Count < 4)
4330
+ {
4331
+ return;
4332
+ }
4840
4333
  handlers[3](ref typedMessage);
4841
4334
  return;
4842
4335
  }
4843
4336
  case 5:
4844
4337
  {
4845
4338
  handlers[0](ref typedMessage);
4339
+ if (handlers.Count < 2)
4340
+ {
4341
+ return;
4342
+ }
4846
4343
  handlers[1](ref typedMessage);
4344
+ if (handlers.Count < 3)
4345
+ {
4346
+ return;
4347
+ }
4847
4348
  handlers[2](ref typedMessage);
4349
+ if (handlers.Count < 4)
4350
+ {
4351
+ return;
4352
+ }
4848
4353
  handlers[3](ref typedMessage);
4354
+ if (handlers.Count < 5)
4355
+ {
4356
+ return;
4357
+ }
4849
4358
  handlers[4](ref typedMessage);
4850
4359
  return;
4851
4360
  }
4852
4361
  }
4853
4362
 
4854
- for (int i = 0; i < handlersCount; ++i)
4363
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
4855
4364
  {
4856
4365
  handlers[i](ref typedMessage);
4857
4366
  }
@@ -4880,7 +4389,7 @@ namespace DxMessaging.Core
4880
4389
  return;
4881
4390
  }
4882
4391
 
4883
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
4392
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
4884
4393
  List<FastHandler<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
4885
4394
  int handlersCount = handlers.Count;
4886
4395
  switch (handlersCount)
@@ -4893,36 +4402,76 @@ namespace DxMessaging.Core
4893
4402
  case 2:
4894
4403
  {
4895
4404
  handlers[0](ref typedMessage);
4405
+ if (handlers.Count < 2)
4406
+ {
4407
+ return;
4408
+ }
4896
4409
  handlers[1](ref typedMessage);
4897
4410
  return;
4898
4411
  }
4899
4412
  case 3:
4900
4413
  {
4901
4414
  handlers[0](ref typedMessage);
4415
+ if (handlers.Count < 2)
4416
+ {
4417
+ return;
4418
+ }
4902
4419
  handlers[1](ref typedMessage);
4420
+ if (handlers.Count < 3)
4421
+ {
4422
+ return;
4423
+ }
4903
4424
  handlers[2](ref typedMessage);
4904
4425
  return;
4905
4426
  }
4906
4427
  case 4:
4907
4428
  {
4908
4429
  handlers[0](ref typedMessage);
4430
+ if (handlers.Count < 2)
4431
+ {
4432
+ return;
4433
+ }
4909
4434
  handlers[1](ref typedMessage);
4435
+ if (handlers.Count < 3)
4436
+ {
4437
+ return;
4438
+ }
4910
4439
  handlers[2](ref typedMessage);
4440
+ if (handlers.Count < 4)
4441
+ {
4442
+ return;
4443
+ }
4911
4444
  handlers[3](ref typedMessage);
4912
4445
  return;
4913
4446
  }
4914
4447
  case 5:
4915
4448
  {
4916
4449
  handlers[0](ref typedMessage);
4450
+ if (handlers.Count < 2)
4451
+ {
4452
+ return;
4453
+ }
4917
4454
  handlers[1](ref typedMessage);
4455
+ if (handlers.Count < 3)
4456
+ {
4457
+ return;
4458
+ }
4918
4459
  handlers[2](ref typedMessage);
4460
+ if (handlers.Count < 4)
4461
+ {
4462
+ return;
4463
+ }
4919
4464
  handlers[3](ref typedMessage);
4465
+ if (handlers.Count < 5)
4466
+ {
4467
+ return;
4468
+ }
4920
4469
  handlers[4](ref typedMessage);
4921
4470
  return;
4922
4471
  }
4923
4472
  }
4924
4473
 
4925
- for (int i = 0; i < handlersCount; ++i)
4474
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
4926
4475
  {
4927
4476
  handlers[i](ref typedMessage);
4928
4477
  }
@@ -4956,7 +4505,7 @@ namespace DxMessaging.Core
4956
4505
  return;
4957
4506
  }
4958
4507
 
4959
- ref TU typedMessage = ref Unsafe.As<TMessage, TU>(ref message);
4508
+ ref TU typedMessage = ref DxUnsafe.As<TMessage, TU>(ref message);
4960
4509
  List<FastHandler<TU>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
4961
4510
  int handlersCount = handlers.Count;
4962
4511
  if (handlersCount == 0)
@@ -4973,36 +4522,76 @@ namespace DxMessaging.Core
4973
4522
  case 2:
4974
4523
  {
4975
4524
  handlers[0](ref typedMessage);
4525
+ if (handlers.Count < 2)
4526
+ {
4527
+ return;
4528
+ }
4976
4529
  handlers[1](ref typedMessage);
4977
4530
  return;
4978
4531
  }
4979
4532
  case 3:
4980
4533
  {
4981
4534
  handlers[0](ref typedMessage);
4535
+ if (handlers.Count < 2)
4536
+ {
4537
+ return;
4538
+ }
4982
4539
  handlers[1](ref typedMessage);
4540
+ if (handlers.Count < 3)
4541
+ {
4542
+ return;
4543
+ }
4983
4544
  handlers[2](ref typedMessage);
4984
4545
  return;
4985
4546
  }
4986
4547
  case 4:
4987
4548
  {
4988
4549
  handlers[0](ref typedMessage);
4550
+ if (handlers.Count < 2)
4551
+ {
4552
+ return;
4553
+ }
4989
4554
  handlers[1](ref typedMessage);
4555
+ if (handlers.Count < 3)
4556
+ {
4557
+ return;
4558
+ }
4990
4559
  handlers[2](ref typedMessage);
4560
+ if (handlers.Count < 4)
4561
+ {
4562
+ return;
4563
+ }
4991
4564
  handlers[3](ref typedMessage);
4992
4565
  return;
4993
4566
  }
4994
4567
  case 5:
4995
4568
  {
4996
4569
  handlers[0](ref typedMessage);
4570
+ if (handlers.Count < 2)
4571
+ {
4572
+ return;
4573
+ }
4997
4574
  handlers[1](ref typedMessage);
4575
+ if (handlers.Count < 3)
4576
+ {
4577
+ return;
4578
+ }
4998
4579
  handlers[2](ref typedMessage);
4580
+ if (handlers.Count < 4)
4581
+ {
4582
+ return;
4583
+ }
4999
4584
  handlers[3](ref typedMessage);
4585
+ if (handlers.Count < 5)
4586
+ {
4587
+ return;
4588
+ }
5000
4589
  handlers[4](ref typedMessage);
5001
4590
  return;
5002
4591
  }
5003
4592
  }
5004
4593
 
5005
- for (int i = 0; i < handlersCount; ++i)
4594
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
5006
4595
  {
5007
4596
  handlers[i](ref typedMessage);
5008
4597
  }
@@ -5025,7 +4614,7 @@ namespace DxMessaging.Core
5025
4614
  return;
5026
4615
  }
5027
4616
 
5028
- ref TU typedMessage = ref Unsafe.As<TMessage, TU>(ref message);
4617
+ ref TU typedMessage = ref DxUnsafe.As<TMessage, TU>(ref message);
5029
4618
  List<FastHandlerWithContext<TU>> handlers = GetOrAddNewHandlerStack(
5030
4619
  cache,
5031
4620
  emissionId
@@ -5045,36 +4634,76 @@ namespace DxMessaging.Core
5045
4634
  case 2:
5046
4635
  {
5047
4636
  handlers[0](ref context, ref typedMessage);
4637
+ if (handlers.Count < 2)
4638
+ {
4639
+ return;
4640
+ }
5048
4641
  handlers[1](ref context, ref typedMessage);
5049
4642
  return;
5050
4643
  }
5051
4644
  case 3:
5052
4645
  {
5053
4646
  handlers[0](ref context, ref typedMessage);
4647
+ if (handlers.Count < 2)
4648
+ {
4649
+ return;
4650
+ }
5054
4651
  handlers[1](ref context, ref typedMessage);
4652
+ if (handlers.Count < 3)
4653
+ {
4654
+ return;
4655
+ }
5055
4656
  handlers[2](ref context, ref typedMessage);
5056
4657
  return;
5057
4658
  }
5058
4659
  case 4:
5059
4660
  {
5060
4661
  handlers[0](ref context, ref typedMessage);
4662
+ if (handlers.Count < 2)
4663
+ {
4664
+ return;
4665
+ }
5061
4666
  handlers[1](ref context, ref typedMessage);
4667
+ if (handlers.Count < 3)
4668
+ {
4669
+ return;
4670
+ }
5062
4671
  handlers[2](ref context, ref typedMessage);
4672
+ if (handlers.Count < 4)
4673
+ {
4674
+ return;
4675
+ }
5063
4676
  handlers[3](ref context, ref typedMessage);
5064
4677
  return;
5065
4678
  }
5066
4679
  case 5:
5067
4680
  {
5068
4681
  handlers[0](ref context, ref typedMessage);
4682
+ if (handlers.Count < 2)
4683
+ {
4684
+ return;
4685
+ }
5069
4686
  handlers[1](ref context, ref typedMessage);
4687
+ if (handlers.Count < 3)
4688
+ {
4689
+ return;
4690
+ }
5070
4691
  handlers[2](ref context, ref typedMessage);
4692
+ if (handlers.Count < 4)
4693
+ {
4694
+ return;
4695
+ }
5071
4696
  handlers[3](ref context, ref typedMessage);
4697
+ if (handlers.Count < 5)
4698
+ {
4699
+ return;
4700
+ }
5072
4701
  handlers[4](ref context, ref typedMessage);
5073
4702
  return;
5074
4703
  }
5075
4704
  }
5076
4705
 
5077
- for (int i = 0; i < handlersCount; ++i)
4706
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
5078
4707
  {
5079
4708
  handlers[i](ref context, ref typedMessage);
5080
4709
  }
@@ -5156,7 +4785,7 @@ namespace DxMessaging.Core
5156
4785
  return;
5157
4786
  }
5158
4787
 
5159
- ref TU typedMessage = ref Unsafe.As<TMessage, TU>(ref message);
4788
+ ref TU typedMessage = ref DxUnsafe.As<TMessage, TU>(ref message);
5160
4789
  List<FastHandlerWithContext<TU>> handlers = GetOrAddNewHandlerStack(
5161
4790
  cache,
5162
4791
  emissionId
@@ -5172,36 +4801,76 @@ namespace DxMessaging.Core
5172
4801
  case 2:
5173
4802
  {
5174
4803
  handlers[0](ref context, ref typedMessage);
4804
+ if (handlers.Count < 2)
4805
+ {
4806
+ return;
4807
+ }
5175
4808
  handlers[1](ref context, ref typedMessage);
5176
4809
  return;
5177
4810
  }
5178
4811
  case 3:
5179
4812
  {
5180
4813
  handlers[0](ref context, ref typedMessage);
4814
+ if (handlers.Count < 2)
4815
+ {
4816
+ return;
4817
+ }
5181
4818
  handlers[1](ref context, ref typedMessage);
4819
+ if (handlers.Count < 3)
4820
+ {
4821
+ return;
4822
+ }
5182
4823
  handlers[2](ref context, ref typedMessage);
5183
4824
  return;
5184
4825
  }
5185
4826
  case 4:
5186
4827
  {
5187
4828
  handlers[0](ref context, ref typedMessage);
4829
+ if (handlers.Count < 2)
4830
+ {
4831
+ return;
4832
+ }
5188
4833
  handlers[1](ref context, ref typedMessage);
4834
+ if (handlers.Count < 3)
4835
+ {
4836
+ return;
4837
+ }
5189
4838
  handlers[2](ref context, ref typedMessage);
4839
+ if (handlers.Count < 4)
4840
+ {
4841
+ return;
4842
+ }
5190
4843
  handlers[3](ref context, ref typedMessage);
5191
4844
  return;
5192
4845
  }
5193
4846
  case 5:
5194
4847
  {
5195
4848
  handlers[0](ref context, ref typedMessage);
4849
+ if (handlers.Count < 2)
4850
+ {
4851
+ return;
4852
+ }
5196
4853
  handlers[1](ref context, ref typedMessage);
4854
+ if (handlers.Count < 3)
4855
+ {
4856
+ return;
4857
+ }
5197
4858
  handlers[2](ref context, ref typedMessage);
4859
+ if (handlers.Count < 4)
4860
+ {
4861
+ return;
4862
+ }
5198
4863
  handlers[3](ref context, ref typedMessage);
4864
+ if (handlers.Count < 5)
4865
+ {
4866
+ return;
4867
+ }
5199
4868
  handlers[4](ref context, ref typedMessage);
5200
4869
  return;
5201
4870
  }
5202
4871
  }
5203
4872
 
5204
- for (int i = 0; i < handlersCount; ++i)
4873
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
5205
4874
  {
5206
4875
  handlers[i](ref context, ref typedMessage);
5207
4876
  }
@@ -5278,7 +4947,7 @@ namespace DxMessaging.Core
5278
4947
  }
5279
4948
 
5280
4949
  List<Action<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
5281
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
4950
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5282
4951
  int handlersCount = handlers.Count;
5283
4952
  switch (handlersCount)
5284
4953
  {
@@ -5290,36 +4959,76 @@ namespace DxMessaging.Core
5290
4959
  case 2:
5291
4960
  {
5292
4961
  handlers[0](typedMessage);
4962
+ if (handlers.Count < 2)
4963
+ {
4964
+ return;
4965
+ }
5293
4966
  handlers[1](typedMessage);
5294
4967
  return;
5295
4968
  }
5296
4969
  case 3:
5297
4970
  {
5298
4971
  handlers[0](typedMessage);
4972
+ if (handlers.Count < 2)
4973
+ {
4974
+ return;
4975
+ }
5299
4976
  handlers[1](typedMessage);
4977
+ if (handlers.Count < 3)
4978
+ {
4979
+ return;
4980
+ }
5300
4981
  handlers[2](typedMessage);
5301
4982
  return;
5302
4983
  }
5303
4984
  case 4:
5304
4985
  {
5305
4986
  handlers[0](typedMessage);
4987
+ if (handlers.Count < 2)
4988
+ {
4989
+ return;
4990
+ }
5306
4991
  handlers[1](typedMessage);
4992
+ if (handlers.Count < 3)
4993
+ {
4994
+ return;
4995
+ }
5307
4996
  handlers[2](typedMessage);
4997
+ if (handlers.Count < 4)
4998
+ {
4999
+ return;
5000
+ }
5308
5001
  handlers[3](typedMessage);
5309
5002
  return;
5310
5003
  }
5311
5004
  case 5:
5312
5005
  {
5313
5006
  handlers[0](typedMessage);
5007
+ if (handlers.Count < 2)
5008
+ {
5009
+ return;
5010
+ }
5314
5011
  handlers[1](typedMessage);
5012
+ if (handlers.Count < 3)
5013
+ {
5014
+ return;
5015
+ }
5315
5016
  handlers[2](typedMessage);
5017
+ if (handlers.Count < 4)
5018
+ {
5019
+ return;
5020
+ }
5316
5021
  handlers[3](typedMessage);
5022
+ if (handlers.Count < 5)
5023
+ {
5024
+ return;
5025
+ }
5317
5026
  handlers[4](typedMessage);
5318
5027
  return;
5319
5028
  }
5320
5029
  }
5321
5030
 
5322
- for (int i = 0; i < handlersCount; ++i)
5031
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
5323
5032
  {
5324
5033
  handlers[i](typedMessage);
5325
5034
  }
@@ -5344,7 +5053,7 @@ namespace DxMessaging.Core
5344
5053
  }
5345
5054
 
5346
5055
  List<Action<T>> handlers = GetOrAddNewHandlerStack(cache, emissionId);
5347
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
5056
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5348
5057
  int handlersCount = handlers.Count;
5349
5058
  switch (handlersCount)
5350
5059
  {
@@ -5356,36 +5065,76 @@ namespace DxMessaging.Core
5356
5065
  case 2:
5357
5066
  {
5358
5067
  handlers[0](typedMessage);
5068
+ if (handlers.Count < 2)
5069
+ {
5070
+ return;
5071
+ }
5359
5072
  handlers[1](typedMessage);
5360
5073
  return;
5361
5074
  }
5362
5075
  case 3:
5363
5076
  {
5364
5077
  handlers[0](typedMessage);
5078
+ if (handlers.Count < 2)
5079
+ {
5080
+ return;
5081
+ }
5365
5082
  handlers[1](typedMessage);
5083
+ if (handlers.Count < 3)
5084
+ {
5085
+ return;
5086
+ }
5366
5087
  handlers[2](typedMessage);
5367
5088
  return;
5368
5089
  }
5369
5090
  case 4:
5370
5091
  {
5371
5092
  handlers[0](typedMessage);
5093
+ if (handlers.Count < 2)
5094
+ {
5095
+ return;
5096
+ }
5372
5097
  handlers[1](typedMessage);
5098
+ if (handlers.Count < 3)
5099
+ {
5100
+ return;
5101
+ }
5373
5102
  handlers[2](typedMessage);
5103
+ if (handlers.Count < 4)
5104
+ {
5105
+ return;
5106
+ }
5374
5107
  handlers[3](typedMessage);
5375
5108
  return;
5376
5109
  }
5377
5110
  case 5:
5378
5111
  {
5379
5112
  handlers[0](typedMessage);
5113
+ if (handlers.Count < 2)
5114
+ {
5115
+ return;
5116
+ }
5380
5117
  handlers[1](typedMessage);
5118
+ if (handlers.Count < 3)
5119
+ {
5120
+ return;
5121
+ }
5381
5122
  handlers[2](typedMessage);
5123
+ if (handlers.Count < 4)
5124
+ {
5125
+ return;
5126
+ }
5382
5127
  handlers[3](typedMessage);
5128
+ if (handlers.Count < 5)
5129
+ {
5130
+ return;
5131
+ }
5383
5132
  handlers[4](typedMessage);
5384
5133
  return;
5385
5134
  }
5386
5135
  }
5387
5136
 
5388
- for (int i = 0; i < handlersCount; ++i)
5137
+ for (int i = 0; i < handlersCount && i < handlers.Count; ++i)
5389
5138
  {
5390
5139
  handlers[i](typedMessage);
5391
5140
  }
@@ -5417,7 +5166,7 @@ namespace DxMessaging.Core
5417
5166
  cache,
5418
5167
  emissionId
5419
5168
  );
5420
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
5169
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5421
5170
  int handlersCount = typedHandlers.Count;
5422
5171
  switch (handlersCount)
5423
5172
  {
@@ -5429,36 +5178,76 @@ namespace DxMessaging.Core
5429
5178
  case 2:
5430
5179
  {
5431
5180
  typedHandlers[0](context, typedMessage);
5181
+ if (typedHandlers.Count < 2)
5182
+ {
5183
+ return;
5184
+ }
5432
5185
  typedHandlers[1](context, typedMessage);
5433
5186
  return;
5434
5187
  }
5435
5188
  case 3:
5436
5189
  {
5437
5190
  typedHandlers[0](context, typedMessage);
5191
+ if (typedHandlers.Count < 2)
5192
+ {
5193
+ return;
5194
+ }
5438
5195
  typedHandlers[1](context, typedMessage);
5196
+ if (typedHandlers.Count < 3)
5197
+ {
5198
+ return;
5199
+ }
5439
5200
  typedHandlers[2](context, typedMessage);
5440
5201
  return;
5441
5202
  }
5442
5203
  case 4:
5443
5204
  {
5444
5205
  typedHandlers[0](context, typedMessage);
5206
+ if (typedHandlers.Count < 2)
5207
+ {
5208
+ return;
5209
+ }
5445
5210
  typedHandlers[1](context, typedMessage);
5211
+ if (typedHandlers.Count < 3)
5212
+ {
5213
+ return;
5214
+ }
5446
5215
  typedHandlers[2](context, typedMessage);
5216
+ if (typedHandlers.Count < 4)
5217
+ {
5218
+ return;
5219
+ }
5447
5220
  typedHandlers[3](context, typedMessage);
5448
5221
  return;
5449
5222
  }
5450
5223
  case 5:
5451
5224
  {
5452
5225
  typedHandlers[0](context, typedMessage);
5226
+ if (typedHandlers.Count < 2)
5227
+ {
5228
+ return;
5229
+ }
5453
5230
  typedHandlers[1](context, typedMessage);
5231
+ if (typedHandlers.Count < 3)
5232
+ {
5233
+ return;
5234
+ }
5454
5235
  typedHandlers[2](context, typedMessage);
5236
+ if (typedHandlers.Count < 4)
5237
+ {
5238
+ return;
5239
+ }
5455
5240
  typedHandlers[3](context, typedMessage);
5241
+ if (typedHandlers.Count < 5)
5242
+ {
5243
+ return;
5244
+ }
5456
5245
  typedHandlers[4](context, typedMessage);
5457
5246
  return;
5458
5247
  }
5459
5248
  }
5460
5249
 
5461
- for (int i = 0; i < handlersCount; ++i)
5250
+ for (int i = 0; i < handlersCount && i < typedHandlers.Count; ++i)
5462
5251
  {
5463
5252
  typedHandlers[i](context, typedMessage);
5464
5253
  }
@@ -5492,7 +5281,7 @@ namespace DxMessaging.Core
5492
5281
  cache,
5493
5282
  emissionId
5494
5283
  );
5495
- ref T typedMessage = ref Unsafe.As<TMessage, T>(ref message);
5284
+ ref T typedMessage = ref DxUnsafe.As<TMessage, T>(ref message);
5496
5285
  int handlersCount = typedHandlers.Count;
5497
5286
  switch (handlersCount)
5498
5287
  {
@@ -5504,60 +5293,124 @@ namespace DxMessaging.Core
5504
5293
  case 2:
5505
5294
  {
5506
5295
  typedHandlers[0](context, typedMessage);
5296
+ if (typedHandlers.Count < 2)
5297
+ {
5298
+ return;
5299
+ }
5507
5300
  typedHandlers[1](context, typedMessage);
5508
5301
  return;
5509
5302
  }
5510
5303
  case 3:
5511
5304
  {
5512
5305
  typedHandlers[0](context, typedMessage);
5306
+ if (typedHandlers.Count < 2)
5307
+ {
5308
+ return;
5309
+ }
5513
5310
  typedHandlers[1](context, typedMessage);
5311
+ if (typedHandlers.Count < 3)
5312
+ {
5313
+ return;
5314
+ }
5514
5315
  typedHandlers[2](context, typedMessage);
5515
5316
  return;
5516
5317
  }
5517
5318
  case 4:
5518
5319
  {
5519
5320
  typedHandlers[0](context, typedMessage);
5321
+ if (typedHandlers.Count < 2)
5322
+ {
5323
+ return;
5324
+ }
5520
5325
  typedHandlers[1](context, typedMessage);
5326
+ if (typedHandlers.Count < 3)
5327
+ {
5328
+ return;
5329
+ }
5521
5330
  typedHandlers[2](context, typedMessage);
5331
+ if (typedHandlers.Count < 4)
5332
+ {
5333
+ return;
5334
+ }
5522
5335
  typedHandlers[3](context, typedMessage);
5523
5336
  return;
5524
5337
  }
5525
5338
  case 5:
5526
5339
  {
5527
5340
  typedHandlers[0](context, typedMessage);
5341
+ if (typedHandlers.Count < 2)
5342
+ {
5343
+ return;
5344
+ }
5528
5345
  typedHandlers[1](context, typedMessage);
5346
+ if (typedHandlers.Count < 3)
5347
+ {
5348
+ return;
5349
+ }
5529
5350
  typedHandlers[2](context, typedMessage);
5351
+ if (typedHandlers.Count < 4)
5352
+ {
5353
+ return;
5354
+ }
5530
5355
  typedHandlers[3](context, typedMessage);
5356
+ if (typedHandlers.Count < 5)
5357
+ {
5358
+ return;
5359
+ }
5531
5360
  typedHandlers[4](context, typedMessage);
5532
5361
  return;
5533
5362
  }
5534
5363
  }
5535
5364
 
5536
- for (int i = 0; i < handlersCount; ++i)
5365
+ for (int i = 0; i < handlersCount && i < typedHandlers.Count; ++i)
5537
5366
  {
5538
5367
  typedHandlers[i](context, typedMessage);
5539
5368
  }
5540
5369
  }
5541
5370
 
5371
+ // Mid-dispatch clear contract: the List returned here is the LIVE
5372
+ // cache.cache list, not a copy. IHandlerActionCache.Reset() (bus
5373
+ // reset / sweep eviction) clears it IN PLACE, so every dispatch
5374
+ // loop that indexes the returned list re-checks list.Count before
5375
+ // each invocation past the first (and the >5 fallback loops bound
5376
+ // on the live Count). A reset fired from inside a handler then
5377
+ // cleanly stops the in-flight bucket: no peer delegate runs and
5378
+ // nothing throws. The re-check is a single inlined List.Count
5379
+ // field read on data already in cache, so steady-state dispatch
5380
+ // cost is unchanged.
5542
5381
  internal static List<TU> GetOrAddNewHandlerStack<TU>(
5543
5382
  HandlerActionCache<TU> actionCache,
5544
5383
  long emissionId
5545
5384
  )
5546
5385
  {
5386
+ DebugAssertInsertionOrderInSync(actionCache);
5547
5387
  if (actionCache.lastSeenEmissionId != emissionId)
5548
5388
  {
5549
5389
  if (actionCache.version != actionCache.lastSeenVersion)
5550
5390
  {
5391
+ // Rebuild the dispatch snapshot from insertionOrder, NOT from
5392
+ // the entries dictionary: dictionary enumeration order permutes
5393
+ // after Remove/Add churn (freed slots are reused LIFO), while
5394
+ // insertionOrder preserves the documented first-registration
5395
+ // order for equal-priority handlers. This branch only runs on
5396
+ // registration churn (version bump), never on steady-state
5397
+ // dispatch, and allocates nothing (the pooled cache list is
5398
+ // cleared and refilled in place).
5551
5399
  List<TU> list = actionCache.cache;
5552
5400
  list.Clear();
5553
- foreach (
5554
- KeyValuePair<
5555
- TU,
5556
- HandlerActionCache<TU>.Entry
5557
- > kvp in actionCache.entries
5558
- )
5401
+ List<TU> orderedHandlers = actionCache.insertionOrder;
5402
+ int orderedCount = orderedHandlers.Count;
5403
+ for (int i = 0; i < orderedCount; ++i)
5559
5404
  {
5560
- list.Add(kvp.Value.handler);
5405
+ if (
5406
+ actionCache.entries.TryGetValue(
5407
+ orderedHandlers[i],
5408
+ out HandlerActionCache<TU>.Entry entry
5409
+ )
5410
+ )
5411
+ {
5412
+ list.Add(entry.handler);
5413
+ }
5561
5414
  }
5562
5415
  actionCache.lastSeenVersion = actionCache.version;
5563
5416
  }
@@ -5566,37 +5419,25 @@ namespace DxMessaging.Core
5566
5419
  return actionCache.cache;
5567
5420
  }
5568
5421
 
5569
- private static void PrefreezeHandlersForEmission<THandler>(
5570
- Dictionary<int, IHandlerActionCache> handlers,
5571
- int priority,
5572
- long emissionId
5573
- )
5574
- {
5575
- if (
5576
- handlers != null
5577
- && handlers.TryGetValue(priority, out IHandlerActionCache erasedCache)
5578
- && erasedCache is HandlerActionCache<THandler> cache
5579
- )
5580
- {
5581
- cache.prefreezeInvocationCount++;
5582
- _ = GetOrAddNewHandlerStack(cache, emissionId);
5583
- }
5584
- }
5585
-
5586
- private static void PrefreezeHandlersForEmission<THandler>(
5587
- Dictionary<int, HandlerActionCache<THandler>> handlers,
5588
- int priority,
5589
- long emissionId
5422
+ // Asserts insertionOrder stays in lockstep with the entries
5423
+ // dictionary at every dispatch-snapshot read. Drift indicates a
5424
+ // mutation site of HandlerActionCache.entries that forgot to
5425
+ // mirror the change into insertionOrder (AddHandler* family,
5426
+ // deregistration closures, IHandlerActionCache.Reset). Stripped
5427
+ // in Release builds via [Conditional("DEBUG")] -- zero hot-path
5428
+ // cost.
5429
+ [Conditional("DEBUG")]
5430
+ private static void DebugAssertInsertionOrderInSync<TU>(
5431
+ HandlerActionCache<TU> actionCache
5590
5432
  )
5591
5433
  {
5592
- if (
5593
- handlers != null
5594
- && handlers.TryGetValue(priority, out HandlerActionCache<THandler> cache)
5595
- )
5596
- {
5597
- cache.prefreezeInvocationCount++;
5598
- _ = GetOrAddNewHandlerStack(cache, emissionId);
5599
- }
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
+ );
5600
5441
  }
5601
5442
 
5602
5443
  private static Action AddHandler<TU>(
@@ -5632,6 +5473,10 @@ namespace DxMessaging.Core
5632
5473
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5633
5474
 
5634
5475
  cache.entries[originalHandler] = entry;
5476
+ if (firstRegistration)
5477
+ {
5478
+ cache.insertionOrder.Add(originalHandler);
5479
+ }
5635
5480
  cache.version++;
5636
5481
  if (firstRegistration)
5637
5482
  {
@@ -5682,6 +5527,7 @@ namespace DxMessaging.Core
5682
5527
  if (localEntry.count <= 1)
5683
5528
  {
5684
5529
  _ = localCache.entries.Remove(originalHandler);
5530
+ _ = localCache.insertionOrder.Remove(originalHandler);
5685
5531
  localCache.version++;
5686
5532
  localSlot.liveCount--;
5687
5533
  return;
@@ -5744,6 +5590,10 @@ namespace DxMessaging.Core
5744
5590
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5745
5591
 
5746
5592
  cache.entries[originalHandler] = entry;
5593
+ if (firstRegistration)
5594
+ {
5595
+ cache.insertionOrder.Add(originalHandler);
5596
+ }
5747
5597
  cache.version++;
5748
5598
 
5749
5599
  Dictionary<
@@ -5782,6 +5632,7 @@ namespace DxMessaging.Core
5782
5632
  if (localEntry.count <= 1)
5783
5633
  {
5784
5634
  _ = localCache.entries.Remove(originalHandler);
5635
+ _ = localCache.insertionOrder.Remove(originalHandler);
5785
5636
  localCache.version++;
5786
5637
  if (localCache.entries.Count == 0)
5787
5638
  {
@@ -5829,6 +5680,10 @@ namespace DxMessaging.Core
5829
5680
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5830
5681
 
5831
5682
  cache.entries[originalHandler] = entry;
5683
+ if (firstRegistration)
5684
+ {
5685
+ cache.insertionOrder.Add(originalHandler);
5686
+ }
5832
5687
  cache.version++;
5833
5688
 
5834
5689
  HandlerActionCache<TU> localCache = cache;
@@ -5852,6 +5707,7 @@ namespace DxMessaging.Core
5852
5707
  if (localEntry.count <= 1)
5853
5708
  {
5854
5709
  _ = localCache.entries.Remove(originalHandler);
5710
+ _ = localCache.insertionOrder.Remove(originalHandler);
5855
5711
  localCache.version++;
5856
5712
  return;
5857
5713
  }
@@ -5897,6 +5753,10 @@ namespace DxMessaging.Core
5897
5753
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5898
5754
 
5899
5755
  cache.entries[originalHandler] = entry;
5756
+ if (firstRegistration)
5757
+ {
5758
+ cache.insertionOrder.Add(originalHandler);
5759
+ }
5900
5760
  cache.version++;
5901
5761
 
5902
5762
  Dictionary<int, HandlerActionCache<TU>> localHandlers = handlers;
@@ -5925,6 +5785,7 @@ namespace DxMessaging.Core
5925
5785
  if (localEntry.count <= 1)
5926
5786
  {
5927
5787
  _ = localCache.entries.Remove(originalHandler);
5788
+ _ = localCache.insertionOrder.Remove(originalHandler);
5928
5789
  localCache.version++;
5929
5790
  if (localCache.entries.Count == 0)
5930
5791
  {
@@ -5946,13 +5807,17 @@ namespace DxMessaging.Core
5946
5807
  // Variant of AddHandler that preserves the priority key in the dictionary when the last entry is removed.
5947
5808
  // This ensures that during an in-flight emission (where handler stacks are already frozen),
5948
5809
  // subsequent removals do not cause lookups to fail for the current pass.
5810
+ // `flatInvoker` carries the pre-resolved flat-dispatch invoker for
5811
+ // registrations the bus-side flat snapshot consumes (untargeted
5812
+ // handle/post default handlers); see HandlerActionCache.Entry.flatInvoker.
5949
5813
  private Action AddHandlerPreservingPriorityKey<TU>(
5950
5814
  Dictionary<int, IHandlerActionCache> handlers,
5951
5815
  TU originalHandler,
5952
5816
  TU augmentedHandler,
5953
5817
  Action deregistration,
5954
5818
  int priority,
5955
- IMessageBus messageBus
5819
+ IMessageBus messageBus,
5820
+ object flatInvoker = null
5956
5821
  )
5957
5822
  {
5958
5823
  if (
@@ -5976,10 +5841,18 @@ namespace DxMessaging.Core
5976
5841
 
5977
5842
  bool firstRegistration = entry.count == 0;
5978
5843
  entry = firstRegistration
5979
- ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1)
5980
- : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
5844
+ ? new HandlerActionCache<TU>.Entry(augmentedHandler, 1, flatInvoker)
5845
+ : new HandlerActionCache<TU>.Entry(
5846
+ entry.handler,
5847
+ entry.count + 1,
5848
+ entry.flatInvoker
5849
+ );
5981
5850
 
5982
5851
  cache.entries[originalHandler] = entry;
5852
+ if (firstRegistration)
5853
+ {
5854
+ cache.insertionOrder.Add(originalHandler);
5855
+ }
5983
5856
  cache.version++;
5984
5857
  TypedSlot<T> slot = FindPrioritySlot(handlers);
5985
5858
  if (slot != null)
@@ -6055,6 +5928,7 @@ namespace DxMessaging.Core
6055
5928
  if (localEntry.count <= 1)
6056
5929
  {
6057
5930
  _ = localCache.entries.Remove(originalHandler);
5931
+ _ = localCache.insertionOrder.Remove(originalHandler);
6058
5932
  localCache.version++;
6059
5933
  if (localSlot != null)
6060
5934
  {
@@ -6067,7 +5941,8 @@ namespace DxMessaging.Core
6067
5941
 
6068
5942
  localEntry = new HandlerActionCache<TU>.Entry(
6069
5943
  localEntry.handler,
6070
- localEntry.count - 1
5944
+ localEntry.count - 1,
5945
+ localEntry.flatInvoker
6071
5946
  );
6072
5947
 
6073
5948
  localCache.entries[originalHandler] = localEntry;
@@ -6107,6 +5982,10 @@ namespace DxMessaging.Core
6107
5982
  : new HandlerActionCache<TU>.Entry(entry.handler, entry.count + 1);
6108
5983
 
6109
5984
  cache.entries[originalHandler] = entry;
5985
+ if (firstRegistration)
5986
+ {
5987
+ cache.insertionOrder.Add(originalHandler);
5988
+ }
6110
5989
  cache.version++;
6111
5990
 
6112
5991
  Dictionary<int, HandlerActionCache<TU>> localHandlers = handlers;
@@ -6135,6 +6014,7 @@ namespace DxMessaging.Core
6135
6014
  if (localEntry.count <= 1)
6136
6015
  {
6137
6016
  _ = localCache.entries.Remove(originalHandler);
6017
+ _ = localCache.insertionOrder.Remove(originalHandler);
6138
6018
  localCache.version++;
6139
6019
  // Intentionally DO NOT remove the priority key here to preserve
6140
6020
  // the cache handle during an in-flight emission.