com.wallstop-studios.dxmessaging 2.0.0-rc27.3 → 2.0.0-rc27.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ namespace DxMessaging.Core.MessageBus
7
7
  using System.Runtime.CompilerServices;
8
8
  using DataStructure;
9
9
  using Diagnostics;
10
+ using DxMessaging.Core;
10
11
  using Extensions;
11
12
  using Helper;
12
13
  using Messages;
@@ -177,6 +178,7 @@ namespace DxMessaging.Core.MessageBus
177
178
  );
178
179
 
179
180
  private bool _diagnosticsMode = GlobalDiagnosticsMode;
181
+ private bool _loggedReflexiveWarning;
180
182
 
181
183
  public Action RegisterUntargeted<T>(MessageHandler messageHandler, int priority = 0)
182
184
  where T : IUntargetedMessage
@@ -1131,6 +1133,17 @@ namespace DxMessaging.Core.MessageBus
1131
1133
 
1132
1134
  if (typeof(TMessage) == typeof(ReflexiveMessage))
1133
1135
  {
1136
+ if (!_loggedReflexiveWarning)
1137
+ {
1138
+ _loggedReflexiveWarning = true;
1139
+ if (MessagingDebug.enabled)
1140
+ {
1141
+ MessagingDebug.Log(
1142
+ LogLevel.Warn,
1143
+ "ReflexiveMessage dispatch traverses the Unity hierarchy and is significantly slower than typed messages. Prefer targeted or broadcast messages where possible."
1144
+ );
1145
+ }
1146
+ }
1134
1147
  #if UNITY_2017_1_OR_NEWER
1135
1148
  ref ReflexiveMessage reflexiveMessage = ref Unsafe.As<TMessage, ReflexiveMessage>(
1136
1149
  ref typedMessage
@@ -1782,6 +1782,7 @@ namespace DxMessaging.Core
1782
1782
  public long version;
1783
1783
  public long lastSeenVersion = -1;
1784
1784
  public long lastSeenEmissionId;
1785
+ internal int prefreezeInvocationCount;
1785
1786
  }
1786
1787
 
1787
1788
  /// <summary>
@@ -1895,39 +1896,16 @@ namespace DxMessaging.Core
1895
1896
  /// <param name="priority">Priority at which to run the handlers.</param>
1896
1897
  public void HandleUntargeted(ref T message, int priority, long emissionId)
1897
1898
  {
1898
- // Pre-freeze untargeted post-processors for this emission/priority
1899
- if (
1900
- _untargetedPostProcessingFastHandlers != null
1901
- && _untargetedPostProcessingFastHandlers.TryGetValue(priority, out var upf)
1902
- )
1903
- {
1904
- _ = GetOrAddNewHandlerStack(upf, emissionId);
1905
- }
1906
- if (
1907
- _untargetedPostProcessingHandlers != null
1908
- && _untargetedPostProcessingHandlers.TryGetValue(priority, out var up)
1909
- )
1910
- {
1911
- _ = GetOrAddNewHandlerStack(up, emissionId);
1912
- }
1913
- // Pre-freeze post-processors for this emission/priority
1914
- if (
1915
- _untargetedPostProcessingFastHandlers != null
1916
- && _untargetedPostProcessingFastHandlers.TryGetValue(
1917
- priority,
1918
- out var fastCache
1919
- )
1920
- )
1921
- {
1922
- _ = GetOrAddNewHandlerStack(fastCache, emissionId);
1923
- }
1924
- if (
1925
- _untargetedPostProcessingHandlers != null
1926
- && _untargetedPostProcessingHandlers.TryGetValue(priority, out var cache)
1927
- )
1928
- {
1929
- _ = GetOrAddNewHandlerStack(cache, emissionId);
1930
- }
1899
+ PrefreezeHandlersForEmission(
1900
+ _untargetedPostProcessingFastHandlers,
1901
+ priority,
1902
+ emissionId
1903
+ );
1904
+ PrefreezeHandlersForEmission(
1905
+ _untargetedPostProcessingHandlers,
1906
+ priority,
1907
+ emissionId
1908
+ );
1931
1909
 
1932
1910
  RunFastHandlers(_untargetedFastHandlers, ref message, priority, emissionId);
1933
1911
  RunHandlers(_untargetedHandlers, ref message, priority, emissionId);
@@ -3535,6 +3513,22 @@ namespace DxMessaging.Core
3535
3513
  return actionCache.cache;
3536
3514
  }
3537
3515
 
3516
+ private static void PrefreezeHandlersForEmission<THandler>(
3517
+ Dictionary<int, HandlerActionCache<THandler>> handlers,
3518
+ int priority,
3519
+ long emissionId
3520
+ )
3521
+ {
3522
+ if (
3523
+ handlers != null
3524
+ && handlers.TryGetValue(priority, out HandlerActionCache<THandler> cache)
3525
+ )
3526
+ {
3527
+ cache.prefreezeInvocationCount++;
3528
+ _ = GetOrAddNewHandlerStack(cache, emissionId);
3529
+ }
3530
+ }
3531
+
3538
3532
  private static Action AddHandler<TU>(
3539
3533
  InstanceId context,
3540
3534
  ref Dictionary<
@@ -59,6 +59,7 @@ namespace DxMessaging.Core
59
59
 
60
60
  private readonly Dictionary<MessageRegistrationHandle, Action> _registrations = new();
61
61
  private readonly Dictionary<MessageRegistrationHandle, Action> _deregistrations = new();
62
+ private readonly List<Action> _deregistrationQueue = new();
62
63
  internal readonly Dictionary<
63
64
  MessageRegistrationHandle,
64
65
  MessageRegistrationMetadata
@@ -1878,13 +1879,11 @@ namespace DxMessaging.Core
1878
1879
 
1879
1880
  if (_deregistrations is { Count: > 0 })
1880
1881
  {
1881
- Dictionary<
1882
- MessageRegistrationHandle,
1883
- Action
1884
- >.ValueCollection.Enumerator enumerator = _deregistrations.Values.GetEnumerator();
1885
- while (enumerator.MoveNext())
1882
+ _deregistrationQueue.Clear();
1883
+ _deregistrationQueue.AddRange(_deregistrations.Values);
1884
+ foreach (Action deregistration in _deregistrationQueue)
1886
1885
  {
1887
- enumerator.Current();
1886
+ deregistration?.Invoke();
1888
1887
  }
1889
1888
  }
1890
1889
 
@@ -1905,15 +1904,13 @@ namespace DxMessaging.Core
1905
1904
  /// </example>
1906
1905
  public void UnregisterAll()
1907
1906
  {
1908
- if (_enabled && _deregistrations is { Count: > 0 })
1907
+ if (_deregistrations is { Count: > 0 })
1909
1908
  {
1910
- Dictionary<
1911
- MessageRegistrationHandle,
1912
- Action
1913
- >.ValueCollection.Enumerator enumerator = _deregistrations.Values.GetEnumerator();
1914
- while (enumerator.MoveNext())
1909
+ _deregistrationQueue.Clear();
1910
+ _deregistrationQueue.AddRange(_deregistrations.Values);
1911
+ foreach (Action deregistration in _deregistrationQueue)
1915
1912
  {
1916
- enumerator.Current();
1913
+ deregistration?.Invoke();
1917
1914
  }
1918
1915
  }
1919
1916
 
@@ -1934,16 +1931,10 @@ namespace DxMessaging.Core
1934
1931
  /// </example>
1935
1932
  public void RemoveRegistration(MessageRegistrationHandle handle)
1936
1933
  {
1937
- if (
1938
- _deregistrations != null
1939
- && _deregistrations.TryGetValue(handle, out Action deregistrationAction)
1940
- )
1934
+ if (_deregistrations?.Remove(handle, out Action deregistrationAction) == true)
1941
1935
  {
1942
- deregistrationAction();
1943
- _ = _deregistrations.Remove(handle);
1936
+ deregistrationAction?.Invoke();
1944
1937
  }
1945
-
1946
- _ = _registrations?.Remove(handle);
1947
1938
  }
1948
1939
 
1949
1940
  /// <summary>
@@ -124,8 +124,14 @@ namespace DxMessaging.Unity
124
124
  /// </summary>
125
125
  protected virtual void OnDestroy()
126
126
  {
127
+ if (_messagingComponent != null)
128
+ {
129
+ _messagingComponent.Release(this);
130
+ }
131
+
127
132
  _messageRegistrationToken?.Disable();
128
133
  _messageRegistrationToken = null;
134
+ _messagingComponent = null;
129
135
  }
130
136
 
131
137
  /// <summary>
@@ -73,6 +73,30 @@ namespace DxMessaging.Unity
73
73
  return createdToken;
74
74
  }
75
75
 
76
+ /// <summary>
77
+ /// Releases the registration token previously created for <paramref name="listener"/>.
78
+ /// </summary>
79
+ /// <param name="listener">Listener whose token should be released.</param>
80
+ /// <remarks>
81
+ /// Invokes <see cref="MessageRegistrationToken.Disable"/> and removes the listener from the internal cache.
82
+ /// Safe to call multiple times.
83
+ /// </remarks>
84
+ public bool Release(MonoBehaviour listener)
85
+ {
86
+ if (listener is null)
87
+ {
88
+ return false;
89
+ }
90
+
91
+ if (_registeredListeners.Remove(listener, out MessageRegistrationToken token))
92
+ {
93
+ token?.Disable();
94
+ return true;
95
+ }
96
+
97
+ return false;
98
+ }
99
+
76
100
  /// <summary>
77
101
  /// Ensures the underlying <see cref="Core.MessageHandler"/> exists.
78
102
  /// </summary>
@@ -0,0 +1,89 @@
1
+ namespace DxMessaging.Tests.Runtime.Core
2
+ {
3
+ using System.Collections;
4
+ using DxMessaging.Core;
5
+ using DxMessaging.Tests.Runtime.Scripts.Components;
6
+ using DxMessaging.Unity;
7
+ using NUnit.Framework;
8
+ using UnityEngine;
9
+ using UnityEngine.TestTools;
10
+
11
+ public sealed class MessagingComponentLifecycleTests : MessagingTestBase
12
+ {
13
+ [UnityTest]
14
+ public IEnumerator ReleasesListenerOnDestroy()
15
+ {
16
+ GameObject go = new(
17
+ "Lifecycle",
18
+ typeof(MessagingComponent),
19
+ typeof(SimpleMessageAwareComponent)
20
+ );
21
+ _spawned.Add(go);
22
+
23
+ MessagingComponent messaging = go.GetComponent<MessagingComponent>();
24
+ SimpleMessageAwareComponent listener = go.GetComponent<SimpleMessageAwareComponent>();
25
+
26
+ yield return null;
27
+
28
+ Assert.AreEqual(
29
+ 1,
30
+ messaging._registeredListeners.Count,
31
+ "Expected initial listener registration."
32
+ );
33
+
34
+ Object.Destroy(listener);
35
+ yield return null;
36
+
37
+ Assert.AreEqual(
38
+ 0,
39
+ messaging._registeredListeners.Count,
40
+ "Listener dictionary should be cleared after destroy."
41
+ );
42
+
43
+ SimpleMessageAwareComponent replacement =
44
+ go.AddComponent<SimpleMessageAwareComponent>();
45
+ yield return null;
46
+
47
+ Assert.AreEqual(
48
+ 1,
49
+ messaging._registeredListeners.Count,
50
+ "Replacement listener should be tracked."
51
+ );
52
+ Assert.IsTrue(messaging._registeredListeners.ContainsKey(replacement));
53
+ }
54
+
55
+ [UnityTest]
56
+ public IEnumerator ManualReleaseRemovesListenerAndDisablesToken()
57
+ {
58
+ GameObject go = new(
59
+ "ManualRelease",
60
+ typeof(MessagingComponent),
61
+ typeof(ManualListenerComponent)
62
+ );
63
+ _spawned.Add(go);
64
+
65
+ MessagingComponent messaging = go.GetComponent<MessagingComponent>();
66
+ ManualListenerComponent listener = go.GetComponent<ManualListenerComponent>();
67
+
68
+ MessageRegistrationToken token = listener.RequestToken(messaging);
69
+ Assert.AreEqual(
70
+ 1,
71
+ messaging._registeredListeners.Count,
72
+ "Token request should register listener."
73
+ );
74
+
75
+ token.Enable();
76
+ Assert.IsTrue(token.Enabled, "Token should enable successfully.");
77
+
78
+ messaging.Release(listener);
79
+ yield return null;
80
+
81
+ Assert.AreEqual(
82
+ 0,
83
+ messaging._registeredListeners.Count,
84
+ "Manual release should remove listener."
85
+ );
86
+ Assert.IsFalse(token.Enabled, "Released token should be disabled.");
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 23c1f6cf389e77147b1fa5edfeb1652f
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,86 @@
1
+ namespace DxMessaging.Tests.Runtime.Core
2
+ {
3
+ using System;
4
+ using System.Collections;
5
+ using System.Collections.Generic;
6
+ using System.Linq;
7
+ using DxMessaging.Core;
8
+ using DxMessaging.Core.MessageBus;
9
+ using DxMessaging.Core.Messages;
10
+ using DxMessaging.Tests.Runtime.Scripts.Components;
11
+ using NUnit.Framework;
12
+ using UnityEngine;
13
+ using UnityEngine.TestTools;
14
+
15
+ public sealed class ReflexiveMessageWarningTests : MessagingTestBase
16
+ {
17
+ [UnityTest]
18
+ public IEnumerator LogsWarningOncePerBus()
19
+ {
20
+ List<(LogLevel level, string message)> logs = new();
21
+ var previousLogFunction = MessagingDebug.LogFunction;
22
+ try
23
+ {
24
+ MessagingDebug.LogFunction = (level, message) => logs.Add((level, message));
25
+ MessagingDebug.enabled = true;
26
+ logs.Clear();
27
+
28
+ GameObject go = new("ReflexiveReceiver", typeof(ReflexiveReceiverComponent));
29
+ _spawned.Add(go);
30
+ ReflexiveReceiverComponent receiver = go.GetComponent<ReflexiveReceiverComponent>();
31
+
32
+ MessageBus bus = new();
33
+ ReflexiveMessage message = new("OnReflexive", ReflexiveSendMode.Flat);
34
+
35
+ int warningsBefore = CountWarnings(logs);
36
+ InstanceId target = receiver;
37
+ bus.TargetedBroadcast(ref target, ref message);
38
+ Assert.AreEqual(1, receiver.InvocationCount);
39
+ int warningsAfter = CountWarnings(logs);
40
+ Assert.Greater(
41
+ warningsAfter,
42
+ warningsBefore,
43
+ "First reflexive dispatch should log a warning."
44
+ );
45
+ StringAssert.Contains("ReflexiveMessage", logs[^1].message);
46
+
47
+ warningsBefore = warningsAfter;
48
+ target = receiver;
49
+ bus.TargetedBroadcast(ref target, ref message);
50
+ Assert.AreEqual(2, receiver.InvocationCount);
51
+ warningsAfter = CountWarnings(logs);
52
+ Assert.AreEqual(
53
+ warningsBefore,
54
+ warningsAfter,
55
+ "Second dispatch on the same bus should not emit additional warnings."
56
+ );
57
+
58
+ MessageBus secondBus = new();
59
+ warningsBefore = warningsAfter;
60
+ target = receiver;
61
+ secondBus.TargetedBroadcast(ref target, ref message);
62
+ Assert.AreEqual(3, receiver.InvocationCount);
63
+ warningsAfter = CountWarnings(logs);
64
+ Assert.AreEqual(
65
+ warningsBefore + 1,
66
+ warningsAfter,
67
+ "A new bus should emit its own warning."
68
+ );
69
+ }
70
+ finally
71
+ {
72
+ MessagingDebug.LogFunction = previousLogFunction;
73
+ }
74
+
75
+ yield break;
76
+ }
77
+
78
+ private static int CountWarnings(List<(LogLevel level, string message)> logs)
79
+ {
80
+ return logs.Count(entry =>
81
+ entry.level == LogLevel.Warn
82
+ && entry.message.IndexOf("ReflexiveMessage dispatch", StringComparison.Ordinal) >= 0
83
+ );
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 9682cda4e2ef15a48869a68d1810cebe
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,116 @@
1
+ namespace DxMessaging.Tests.Runtime.Core
2
+ {
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using System.Reflection;
6
+ using DxMessaging.Core;
7
+ using DxMessaging.Core.Helper;
8
+ using DxMessaging.Core.MessageBus;
9
+ using DxMessaging.Tests.Runtime.Scripts.Messages;
10
+ using NUnit.Framework;
11
+
12
+ public sealed class UntargetedPrefreezeTests
13
+ {
14
+ [Test]
15
+ public void PrefreezeRunsOncePerEmission()
16
+ {
17
+ MessageHandler handler = new(new InstanceId(123)) { active = true };
18
+ MessageBus messageBus = new();
19
+ MessageRegistrationToken token = MessageRegistrationToken.Create(handler, messageBus);
20
+
21
+ int postProcessCount = 0;
22
+ _ = token.RegisterUntargeted<SimpleUntargetedMessage>(
23
+ (ref SimpleUntargetedMessage _) => { }
24
+ );
25
+ _ = token.RegisterUntargetedPostProcessor<SimpleUntargetedMessage>(
26
+ (ref SimpleUntargetedMessage _) => postProcessCount++,
27
+ priority: 0
28
+ );
29
+
30
+ token.Enable();
31
+
32
+ object cache = GetUntargetedPostProcessingFastCache(
33
+ handler,
34
+ messageBus,
35
+ typeof(SimpleUntargetedMessage),
36
+ priority: 0
37
+ );
38
+
39
+ SimpleUntargetedMessage message = new();
40
+ messageBus.UntargetedBroadcast(ref message);
41
+ Assert.AreEqual(1, postProcessCount);
42
+ Assert.AreEqual(1, GetPrefreezeInvocationCount(cache));
43
+
44
+ messageBus.UntargetedBroadcast(ref message);
45
+ Assert.AreEqual(2, postProcessCount);
46
+ Assert.AreEqual(2, GetPrefreezeInvocationCount(cache));
47
+
48
+ token.Disable();
49
+ }
50
+
51
+ private static object GetUntargetedPostProcessingFastCache(
52
+ MessageHandler handler,
53
+ MessageBus messageBus,
54
+ Type messageType,
55
+ int priority
56
+ )
57
+ {
58
+ FieldInfo handlersField = typeof(MessageHandler).GetField(
59
+ "_handlersByTypeByMessageBus",
60
+ BindingFlags.NonPublic | BindingFlags.Instance
61
+ );
62
+ Assert.IsNotNull(handlersField);
63
+ var handlersByBus = (List<MessageCache<object>>)handlersField.GetValue(handler);
64
+ Assert.IsNotNull(handlersByBus);
65
+
66
+ MessageCache<object> cacheByType = handlersByBus[
67
+ messageBus.RegisteredGlobalSequentialIndex
68
+ ];
69
+ MethodInfo getOrAddMethod = typeof(MessageCache<object>)
70
+ .GetMethod(nameof(MessageCache<object>.GetOrAdd))
71
+ ?.MakeGenericMethod(messageType);
72
+ Assert.IsNotNull(getOrAddMethod);
73
+
74
+ object typedHandler = getOrAddMethod.Invoke(cacheByType, null);
75
+ Assert.IsNotNull(typedHandler);
76
+
77
+ FieldInfo fastHandlersField = typedHandler
78
+ .GetType()
79
+ .GetField(
80
+ "_untargetedPostProcessingFastHandlers",
81
+ BindingFlags.NonPublic | BindingFlags.Instance
82
+ );
83
+ Assert.IsNotNull(fastHandlersField);
84
+
85
+ object fastHandlers = fastHandlersField.GetValue(typedHandler);
86
+ Assert.IsNotNull(fastHandlers);
87
+
88
+ return GetDictionaryValue(fastHandlers, priority);
89
+ }
90
+
91
+ private static object GetDictionaryValue(object dictionary, int key)
92
+ {
93
+ MethodInfo tryGetValue = dictionary
94
+ .GetType()
95
+ .GetMethod("TryGetValue", BindingFlags.Public | BindingFlags.Instance);
96
+ Assert.IsNotNull(tryGetValue);
97
+
98
+ object[] args = { key, null };
99
+ bool found = (bool)tryGetValue.Invoke(dictionary, args);
100
+ Assert.IsTrue(found, $"Failed to locate cache for priority {key}.");
101
+ return args[1];
102
+ }
103
+
104
+ private static int GetPrefreezeInvocationCount(object handlerCache)
105
+ {
106
+ FieldInfo countField = handlerCache
107
+ .GetType()
108
+ .GetField(
109
+ "prefreezeInvocationCount",
110
+ BindingFlags.NonPublic | BindingFlags.Instance
111
+ );
112
+ Assert.IsNotNull(countField);
113
+ return (int)countField.GetValue(handlerCache);
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 9a36705993094444a95042994d092fc3
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,14 @@
1
+ namespace DxMessaging.Tests.Runtime.Scripts.Components
2
+ {
3
+ using UnityEngine;
4
+
5
+ public sealed class ReflexiveReceiverComponent : MonoBehaviour
6
+ {
7
+ public int InvocationCount { get; private set; }
8
+
9
+ public void OnReflexive()
10
+ {
11
+ InvocationCount++;
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: d7c3fab9fe20b6944814c031134be8e6
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.wallstop-studios.dxmessaging",
3
- "version": "2.0.0-rc27.3",
3
+ "version": "2.0.0-rc27.3.1",
4
4
  "displayName": "DxMessaging",
5
5
  "description": "Synchronous Event Bus for Unity",
6
6
  "unity": "2021.3",