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
@@ -2,6 +2,7 @@ namespace DxMessaging.Core
2
2
  {
3
3
  using System;
4
4
  using System.Collections.Generic;
5
+ using System.Runtime.ExceptionServices;
5
6
  using DataStructure;
6
7
  using Diagnostics;
7
8
  using MessageBus;
@@ -58,8 +59,23 @@ namespace DxMessaging.Core
58
59
  private readonly MessageHandler _messageHandler;
59
60
 
60
61
  private readonly Dictionary<MessageRegistrationHandle, Action> _registrations = new();
61
- private readonly Dictionary<MessageRegistrationHandle, Action> _deregistrations = new();
62
+
63
+ // Staged-registration handles in registration order. Dictionary
64
+ // enumeration order is NOT stable across Remove/Add churn, so Enable()
65
+ // (and RetargetMessageBus) replay staged registrations by walking this
66
+ // list instead of _registrations.Values; otherwise a Disable()/Enable()
67
+ // cycle after churn would silently permute the documented
68
+ // "same priority uses registration order" dispatch contract.
69
+ // Invariant: contains exactly the keys of _registrations, in the order
70
+ // InternalRegister staged them (handle ids are monotonically
71
+ // increasing, so this list is also sorted by handle id).
72
+ private readonly List<MessageRegistrationHandle> _registrationOrder = new();
73
+ private readonly Dictionary<
74
+ MessageRegistrationHandle,
75
+ PendingDeregistration
76
+ > _deregistrations = new();
62
77
  private readonly List<Action> _actionQueue = new();
78
+ private readonly List<MessageRegistrationHandle> _handleQueue = new();
63
79
  internal readonly Dictionary<
64
80
  MessageRegistrationHandle,
65
81
  MessageRegistrationMetadata
@@ -1718,7 +1734,12 @@ namespace DxMessaging.Core
1718
1734
  }
1719
1735
 
1720
1736
  return InternalRegister(
1721
- _ => _messageHandler.RegisterUntargetedInterceptor(interceptor, priority),
1737
+ _ =>
1738
+ _messageHandler.RegisterUntargetedInterceptor(
1739
+ interceptor,
1740
+ priority: priority,
1741
+ messageBus: _messageBus
1742
+ ),
1722
1743
  () =>
1723
1744
  new MessageRegistrationMetadata(
1724
1745
  null,
@@ -1748,7 +1769,12 @@ namespace DxMessaging.Core
1748
1769
  }
1749
1770
 
1750
1771
  return InternalRegister(
1751
- _ => _messageHandler.RegisterBroadcastInterceptor(interceptor, priority),
1772
+ _ =>
1773
+ _messageHandler.RegisterBroadcastInterceptor(
1774
+ interceptor,
1775
+ priority: priority,
1776
+ messageBus: _messageBus
1777
+ ),
1752
1778
  () =>
1753
1779
  new MessageRegistrationMetadata(
1754
1780
  null,
@@ -1778,7 +1804,12 @@ namespace DxMessaging.Core
1778
1804
  }
1779
1805
 
1780
1806
  return InternalRegister(
1781
- _ => _messageHandler.RegisterTargetedInterceptor(interceptor, priority),
1807
+ _ =>
1808
+ _messageHandler.RegisterTargetedInterceptor(
1809
+ interceptor,
1810
+ priority: priority,
1811
+ messageBus: _messageBus
1812
+ ),
1782
1813
  () =>
1783
1814
  new MessageRegistrationMetadata(
1784
1815
  null,
@@ -1804,6 +1835,7 @@ namespace DxMessaging.Core
1804
1835
  MessageRegistrationHandle.CreateMessageRegistrationHandle();
1805
1836
 
1806
1837
  _registrations[handle] = Registration;
1838
+ _registrationOrder.Add(handle);
1807
1839
  _metadata[handle] = metadataProducer();
1808
1840
 
1809
1841
  // Generally, registrations should take place before all calls to enable. Just in case, though...
@@ -1818,7 +1850,7 @@ namespace DxMessaging.Core
1818
1850
  void Registration()
1819
1851
  {
1820
1852
  Action actualDeregistration = registerAndGetDeregistration(handle);
1821
- _deregistrations[handle] = actualDeregistration;
1853
+ AddDeregistration(handle, actualDeregistration);
1822
1854
  }
1823
1855
  }
1824
1856
 
@@ -1843,12 +1875,15 @@ namespace DxMessaging.Core
1843
1875
 
1844
1876
  if (_registrations is { Count: > 0 })
1845
1877
  {
1846
- _actionQueue.Clear();
1847
- _actionQueue.AddRange(_registrations.Values);
1848
- foreach (Action action in _actionQueue)
1849
- {
1850
- action();
1851
- }
1878
+ // Replay staged registrations in original registration order
1879
+ // (via _registrationOrder) rather than in
1880
+ // _registrations.Values enumeration order, which permutes
1881
+ // after Remove/Add churn. This preserves the documented
1882
+ // equal-priority "registration order" dispatch contract across
1883
+ // Disable()/Enable() cycles. Snapshot into _actionQueue first
1884
+ // so replay tolerates re-entrant registration mutation.
1885
+ QueueRegistrationsInOrder();
1886
+ InvokeRegistrationQueueWithRollback();
1852
1887
  }
1853
1888
 
1854
1889
  _enabled = true;
@@ -1867,28 +1902,21 @@ namespace DxMessaging.Core
1867
1902
  /// </example>
1868
1903
  public void Disable()
1869
1904
  {
1870
- if (!_enabled)
1905
+ if (!_enabled && _deregistrations.Count == 0)
1871
1906
  {
1872
1907
  return;
1873
1908
  }
1874
1909
 
1875
- if (_deregistrations is { Count: > 0 })
1910
+ Exception deregistrationException = InvokeDeregistrationQueue();
1911
+ _enabled = _deregistrations.Count > 0;
1912
+ if (deregistrationException != null)
1876
1913
  {
1877
- _actionQueue.Clear();
1878
- _actionQueue.AddRange(_deregistrations.Values);
1879
- foreach (Action deregistration in _actionQueue)
1880
- {
1881
- deregistration?.Invoke();
1882
- }
1914
+ ExceptionDispatchInfo.Capture(deregistrationException).Throw();
1883
1915
  }
1884
-
1885
- // ReSharper disable once ForCanBeConvertedToForeach
1886
-
1887
- _enabled = false;
1888
1916
  }
1889
1917
 
1890
1918
  /// <summary>
1891
- /// Disables the token and clears all registrations and de-registrations.
1919
+ /// Disables the token and clears all registrations, de-registrations, and token-local diagnostic state.
1892
1920
  /// </summary>
1893
1921
  /// <example>
1894
1922
  /// <code>
@@ -1899,19 +1927,19 @@ namespace DxMessaging.Core
1899
1927
  /// </example>
1900
1928
  public void UnregisterAll()
1901
1929
  {
1902
- if (_deregistrations is { Count: > 0 })
1930
+ Exception deregistrationException = InvokeDeregistrationQueue();
1931
+ if (deregistrationException == null)
1903
1932
  {
1904
- _actionQueue.Clear();
1905
- _actionQueue.AddRange(_deregistrations.Values);
1906
- foreach (Action deregistration in _actionQueue)
1907
- {
1908
- deregistration?.Invoke();
1909
- }
1933
+ _enabled = false;
1934
+ _registrations?.Clear();
1935
+ _registrationOrder?.Clear();
1936
+ ClearDiagnosticState();
1937
+ return;
1910
1938
  }
1911
1939
 
1912
- _enabled = false;
1913
- _registrations?.Clear();
1914
- _deregistrations?.Clear();
1940
+ _enabled = _deregistrations.Count > 0;
1941
+ PruneRegistrationStateToFailedDeregistrations();
1942
+ ExceptionDispatchInfo.Capture(deregistrationException).Throw();
1915
1943
  }
1916
1944
 
1917
1945
  /// <summary>
@@ -1938,27 +1966,333 @@ namespace DxMessaging.Core
1938
1966
  return;
1939
1967
  }
1940
1968
 
1969
+ IMessageBus previousMessageBus = _messageBus;
1970
+ List<MessageRegistrationHandle> activeRetargetHandles = rebindActiveRegistrations
1971
+ ? new List<MessageRegistrationHandle>(_deregistrations.Keys)
1972
+ : null;
1941
1973
  if (rebindActiveRegistrations)
1942
1974
  {
1943
- _actionQueue.Clear();
1944
- _actionQueue.AddRange(_deregistrations.Values);
1945
- foreach (Action deregistration in _actionQueue)
1975
+ Exception deregistrationException = InvokeDeregistrationQueue();
1976
+ if (deregistrationException != null)
1946
1977
  {
1947
- deregistration?.Invoke();
1978
+ RestoreMissingRegistrationsAfterRetargetDeregistrationFailure(
1979
+ previousMessageBus,
1980
+ activeRetargetHandles
1981
+ );
1982
+ _enabled = _deregistrations.Count > 0;
1983
+ ExceptionDispatchInfo.Capture(deregistrationException).Throw();
1948
1984
  }
1985
+
1986
+ _enabled = false;
1949
1987
  }
1950
1988
 
1951
1989
  _messageBus = messageBus;
1952
1990
 
1953
1991
  if (rebindActiveRegistrations && _registrations is { Count: > 0 })
1992
+ {
1993
+ // Mirror Enable(): rebind in original registration order so the
1994
+ // equal-priority dispatch order survives a bus retarget.
1995
+ QueueRegistrationsInOrder();
1996
+ try
1997
+ {
1998
+ InvokeRegistrationQueueWithRollback();
1999
+ _enabled = true;
2000
+ }
2001
+ catch (Exception exception)
2002
+ {
2003
+ RestoreRegistrationsAfterRetargetFailure(previousMessageBus);
2004
+ ExceptionDispatchInfo.Capture(exception).Throw();
2005
+ throw;
2006
+ }
2007
+ }
2008
+ }
2009
+
2010
+ private void AddDeregistration(MessageRegistrationHandle handle, Action deregistration)
2011
+ {
2012
+ if (!_deregistrations.TryGetValue(handle, out PendingDeregistration pending))
2013
+ {
2014
+ pending = new PendingDeregistration();
2015
+ _deregistrations[handle] = pending;
2016
+ }
2017
+
2018
+ pending.Add(deregistration);
2019
+ }
2020
+
2021
+ private Dictionary<MessageRegistrationHandle, int> SnapshotDeregistrationCounts()
2022
+ {
2023
+ Dictionary<MessageRegistrationHandle, int> snapshot = new(_deregistrations.Count);
2024
+ foreach (
2025
+ KeyValuePair<
2026
+ MessageRegistrationHandle,
2027
+ PendingDeregistration
2028
+ > entry in _deregistrations
2029
+ )
2030
+ {
2031
+ snapshot[entry.Key] = entry.Value.Count;
2032
+ }
2033
+
2034
+ return snapshot;
2035
+ }
2036
+
2037
+ private void RestoreMissingRegistrationsAfterRetargetDeregistrationFailure(
2038
+ IMessageBus previousMessageBus,
2039
+ List<MessageRegistrationHandle> activeRetargetHandles
2040
+ )
2041
+ {
2042
+ _messageBus = previousMessageBus;
2043
+ if (activeRetargetHandles == null || activeRetargetHandles.Count == 0)
2044
+ {
2045
+ return;
2046
+ }
2047
+
2048
+ QueueMissingRegistrationsInOrder(activeRetargetHandles);
2049
+ try
2050
+ {
2051
+ InvokeRegistrationQueueWithRollback();
2052
+ }
2053
+ catch (Exception exception)
2054
+ {
2055
+ if (MessagingDebug.enabled)
2056
+ {
2057
+ MessagingDebug.Log(
2058
+ LogLevel.Error,
2059
+ "Failed to restore registrations after retarget deregistration failure: {0}",
2060
+ exception
2061
+ );
2062
+ }
2063
+ }
2064
+ }
2065
+
2066
+ private void RestoreRegistrationsAfterRetargetFailure(IMessageBus previousMessageBus)
2067
+ {
2068
+ _messageBus = previousMessageBus;
2069
+ if (_registrations.Count == 0)
2070
+ {
2071
+ _enabled = false;
2072
+ return;
2073
+ }
2074
+
2075
+ QueueRegistrationsWithoutRetryableDeregistrationsInOrder();
2076
+ try
2077
+ {
2078
+ InvokeRegistrationQueueWithRollback();
2079
+ _enabled = true;
2080
+ }
2081
+ catch (Exception restoreException)
2082
+ {
2083
+ _enabled = _deregistrations.Count > 0;
2084
+ if (MessagingDebug.enabled)
2085
+ {
2086
+ MessagingDebug.Log(
2087
+ LogLevel.Error,
2088
+ "Failed to restore registrations after retarget replay failure: {0}",
2089
+ restoreException
2090
+ );
2091
+ }
2092
+ }
2093
+ }
2094
+
2095
+ private void QueueRegistrationsInOrder()
2096
+ {
2097
+ _actionQueue.Clear();
2098
+ int registrationCount = _registrationOrder.Count;
2099
+ for (int i = 0; i < registrationCount; ++i)
2100
+ {
2101
+ if (_registrations.TryGetValue(_registrationOrder[i], out Action registration))
2102
+ {
2103
+ _actionQueue.Add(registration);
2104
+ }
2105
+ }
2106
+ }
2107
+
2108
+ private void QueueMissingRegistrationsInOrder(
2109
+ List<MessageRegistrationHandle> activeRetargetHandles
2110
+ )
2111
+ {
2112
+ _actionQueue.Clear();
2113
+ int registrationCount = _registrationOrder.Count;
2114
+ for (int i = 0; i < registrationCount; ++i)
2115
+ {
2116
+ MessageRegistrationHandle handle = _registrationOrder[i];
2117
+ if (
2118
+ !activeRetargetHandles.Contains(handle)
2119
+ || _deregistrations.ContainsKey(handle)
2120
+ || !_registrations.TryGetValue(handle, out Action registration)
2121
+ )
2122
+ {
2123
+ continue;
2124
+ }
2125
+
2126
+ _actionQueue.Add(registration);
2127
+ }
2128
+ }
2129
+
2130
+ private void QueueRegistrationsWithoutRetryableDeregistrationsInOrder()
2131
+ {
2132
+ _actionQueue.Clear();
2133
+ int registrationCount = _registrationOrder.Count;
2134
+ for (int i = 0; i < registrationCount; ++i)
2135
+ {
2136
+ MessageRegistrationHandle handle = _registrationOrder[i];
2137
+ if (
2138
+ _deregistrations.ContainsKey(handle)
2139
+ || !_registrations.TryGetValue(handle, out Action registration)
2140
+ )
2141
+ {
2142
+ continue;
2143
+ }
2144
+
2145
+ _actionQueue.Add(registration);
2146
+ }
2147
+ }
2148
+
2149
+ private void InvokeActionQueue()
2150
+ {
2151
+ try
2152
+ {
2153
+ foreach (Action action in _actionQueue)
2154
+ {
2155
+ action?.Invoke();
2156
+ }
2157
+ }
2158
+ finally
1954
2159
  {
1955
2160
  _actionQueue.Clear();
1956
- _actionQueue.AddRange(_registrations.Values);
1957
- foreach (Action registration in _actionQueue)
2161
+ }
2162
+ }
2163
+
2164
+ private Exception InvokeDeregistrationQueue(
2165
+ Dictionary<MessageRegistrationHandle, int> baselineCounts = null
2166
+ )
2167
+ {
2168
+ if (_deregistrations.Count == 0)
2169
+ {
2170
+ return null;
2171
+ }
2172
+
2173
+ bool scopedToAddedDeregistrations = baselineCounts != null;
2174
+ _handleQueue.Clear();
2175
+ _handleQueue.AddRange(_deregistrations.Keys);
2176
+ Exception firstException = null;
2177
+ try
2178
+ {
2179
+ foreach (MessageRegistrationHandle handle in _handleQueue)
2180
+ {
2181
+ if (!_deregistrations.TryGetValue(handle, out PendingDeregistration pending))
2182
+ {
2183
+ continue;
2184
+ }
2185
+
2186
+ int startIndex = 0;
2187
+ if (scopedToAddedDeregistrations)
2188
+ {
2189
+ if (baselineCounts.TryGetValue(handle, out int baselineCount))
2190
+ {
2191
+ startIndex = baselineCount;
2192
+ }
2193
+
2194
+ if (startIndex >= pending.Count)
2195
+ {
2196
+ continue;
2197
+ }
2198
+ }
2199
+
2200
+ Exception exception = pending.InvokeFrom(startIndex);
2201
+ if (exception != null)
2202
+ {
2203
+ firstException ??= exception;
2204
+ }
2205
+
2206
+ if (pending.Count == 0)
2207
+ {
2208
+ _deregistrations.Remove(handle);
2209
+ }
2210
+ }
2211
+ }
2212
+ finally
2213
+ {
2214
+ _handleQueue.Clear();
2215
+ }
2216
+
2217
+ return firstException;
2218
+ }
2219
+
2220
+ private void InvokeRegistrationQueueWithRollback()
2221
+ {
2222
+ Dictionary<MessageRegistrationHandle, int> rollbackBaseline =
2223
+ SnapshotDeregistrationCounts();
2224
+ try
2225
+ {
2226
+ InvokeActionQueue();
2227
+ }
2228
+ catch (Exception exception)
2229
+ {
2230
+ RollBackDeregistrationsAfterRegistrationFailure(rollbackBaseline);
2231
+ _enabled = _deregistrations.Count > 0;
2232
+ ExceptionDispatchInfo.Capture(exception).Throw();
2233
+ throw;
2234
+ }
2235
+ }
2236
+
2237
+ private void RollBackDeregistrationsAfterRegistrationFailure(
2238
+ Dictionary<MessageRegistrationHandle, int> rollbackBaseline
2239
+ )
2240
+ {
2241
+ if (_deregistrations.Count == 0)
2242
+ {
2243
+ return;
2244
+ }
2245
+
2246
+ Exception rollbackException = InvokeDeregistrationQueue(rollbackBaseline);
2247
+ if (rollbackException != null && MessagingDebug.enabled)
2248
+ {
2249
+ MessagingDebug.Log(
2250
+ LogLevel.Error,
2251
+ "Failed to roll back partial registration after token replay failure: {0}",
2252
+ rollbackException
2253
+ );
2254
+ }
2255
+ }
2256
+
2257
+ private void ClearDiagnosticState()
2258
+ {
2259
+ _metadata.Clear();
2260
+ _callCounts.Clear();
2261
+ _emissionBuffer.Clear();
2262
+ }
2263
+
2264
+ private void PruneRegistrationStateToFailedDeregistrations()
2265
+ {
2266
+ for (int i = _registrationOrder.Count - 1; i >= 0; --i)
2267
+ {
2268
+ MessageRegistrationHandle handle = _registrationOrder[i];
2269
+ if (_deregistrations.ContainsKey(handle))
1958
2270
  {
1959
- registration?.Invoke();
2271
+ continue;
1960
2272
  }
2273
+
2274
+ RemoveRegistrationState(handle);
2275
+ }
2276
+
2277
+ _emissionBuffer.Clear();
2278
+ if (_registrations.Count == 0)
2279
+ {
2280
+ ClearDiagnosticState();
2281
+ }
2282
+ }
2283
+
2284
+ private bool RemoveRegistrationState(MessageRegistrationHandle handle)
2285
+ {
2286
+ bool removedRegistration = _registrations.Remove(handle);
2287
+ _ = _registrationOrder.Remove(handle);
2288
+ _ = _metadata.Remove(handle);
2289
+ _ = _callCounts.Remove(handle);
2290
+ if (removedRegistration && _registrations.Count == 0)
2291
+ {
2292
+ ClearDiagnosticState();
1961
2293
  }
2294
+
2295
+ return removedRegistration;
1962
2296
  }
1963
2297
 
1964
2298
  /// <summary>
@@ -1973,17 +2307,61 @@ namespace DxMessaging.Core
1973
2307
  /// </example>
1974
2308
  public void RemoveRegistration(MessageRegistrationHandle handle)
1975
2309
  {
1976
- if (_deregistrations?.Remove(handle, out Action deregistrationAction) == true)
2310
+ if (_deregistrations.TryGetValue(handle, out PendingDeregistration pending))
1977
2311
  {
1978
- deregistrationAction?.Invoke();
2312
+ Exception deregistrationException = pending.InvokeFrom(0);
2313
+ if (pending.Count == 0)
2314
+ {
2315
+ _deregistrations.Remove(handle);
2316
+ }
2317
+
2318
+ if (deregistrationException != null)
2319
+ {
2320
+ ExceptionDispatchInfo.Capture(deregistrationException).Throw();
2321
+ }
1979
2322
  }
1980
2323
 
1981
2324
  // Drop the matching staged registration and metadata so a later
1982
2325
  // Disable()/Enable() cycle does not silently re-register the
1983
2326
  // handler we were just asked to remove.
1984
- _ = _registrations?.Remove(handle);
1985
- _ = _metadata?.Remove(handle);
1986
- _ = _callCounts?.Remove(handle);
2327
+ RemoveRegistrationState(handle);
2328
+ }
2329
+
2330
+ private sealed class PendingDeregistration
2331
+ {
2332
+ private readonly List<Action> _actions = new();
2333
+
2334
+ internal int Count => _actions.Count;
2335
+
2336
+ internal void Add(Action action)
2337
+ {
2338
+ _actions.Add(action);
2339
+ }
2340
+
2341
+ internal Exception InvokeFrom(int startIndex)
2342
+ {
2343
+ if (startIndex < 0)
2344
+ {
2345
+ startIndex = 0;
2346
+ }
2347
+
2348
+ Exception firstException = null;
2349
+ for (int i = startIndex; i < _actions.Count; )
2350
+ {
2351
+ try
2352
+ {
2353
+ _actions[i]?.Invoke();
2354
+ _actions.RemoveAt(i);
2355
+ }
2356
+ catch (Exception exception)
2357
+ {
2358
+ firstException ??= exception;
2359
+ ++i;
2360
+ }
2361
+ }
2362
+
2363
+ return firstException;
2364
+ }
1987
2365
  }
1988
2366
 
1989
2367
  /// <summary>
@@ -2052,7 +2430,7 @@ namespace DxMessaging.Core
2052
2430
  }
2053
2431
 
2054
2432
  /// <summary>
2055
- /// Removes all staged registrations and releases references to the handler.
2433
+ /// Removes all staged registrations and clears token-local diagnostic state.
2056
2434
  /// </summary>
2057
2435
  public void Dispose()
2058
2436
  {
@@ -1,3 +1,11 @@
1
1
  fileFormatVersion: 2
2
2
  guid: 58b2b780f9844aeb8929eeca855ba3be
3
- timeCreated: 1745161506
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -1,3 +1,11 @@
1
1
  fileFormatVersion: 2
2
2
  guid: f9a83f6bf3c0487f9e5cff498dbb6932
3
- timeCreated: 1745161955
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -1,3 +1,11 @@
1
1
  fileFormatVersion: 2
2
2
  guid: cf8206ca4c8449a8b8e112ad25554f1f
3
- timeCreated: 1745161457
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -1,3 +1,11 @@
1
1
  fileFormatVersion: 2
2
2
  guid: 978915f0cb9d43d99e46455ff38dc528
3
- timeCreated: 1761517485
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -1,3 +1,11 @@
1
1
  fileFormatVersion: 2
2
2
  guid: c26d8cf1c99e45c0a847404d9a455ed1
3
- timeCreated: 1761517479
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -1,3 +1,11 @@
1
1
  fileFormatVersion: 2
2
2
  guid: ec21f9c7970043fe88177114708ef79e
3
- timeCreated: 1761517466
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: