Skip to content

Commit 63af7b0

Browse files
committed
**New**:
- Added *PublishSafe* method to *IMessageBrokerService* to allow publishing messages safely in chase of chain subscriptions during publishing of a message **Changed**: - *Subscribe* and *Unsubscribe* throw an *InvalidOperationException* when being executed during a message being published **Fixed**: - CoroutineTests issues running when building released projects
1 parent 2201cc9 commit 63af7b0

File tree

8 files changed

+113
-62
lines changed

8 files changed

+113
-62
lines changed

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ All notable changes to this package will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7-
## [0.13.2] - 2024-11-13
7+
## [0.14.0] - 2024-11-15
88

99
**New**:
10-
- Added a constructor to *GameObjectPool* that allows to setup a costum instantiator
10+
- Added *PublishSafe* method to *IMessageBrokerService* to allow publishing messages safely in chase of chain subscriptions during publishing of a message
11+
12+
**Changed**:
13+
- *Subscribe* and *Unsubscribe* throw an *InvalidOperationException* when being executed during a message being published
1114

1215
**Fixed**:
13-
- Fixed *ObjectPool* & *PoolService* tests that would block builds sometimes
16+
- CoroutineTests issues running when building released projects
1417

1518
## [0.13.1] - 2024-11-04
1619

Runtime/MessageBrokerService.cs

Lines changed: 64 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,32 @@ public interface IMessageBrokerService
2525
/// Publish a message in the message broker.
2626
/// If there is no object subscribing the message type, nothing will happen
2727
/// </summary>
28+
/// <remarks>
29+
/// Use <see cref="PublishSafe{T}(T)"/> it there are a chain subscriptions during publishing
30+
/// </remarks>
2831
void Publish<T>(T message) where T : IMessage;
29-
32+
33+
/// <summary>
34+
/// Publish a message in the message broker.
35+
/// If there is no object subscribing the message type, nothing will happen
36+
/// </summary>
37+
/// <remarks>
38+
/// This method can be slow and allocated extra memory if there are a lot of subscribers to the <typeparamref name="T"/>.
39+
/// Use <see cref="Publish{T}(T)"/> instead for faster iteration speed IF and ONLY IF there aren't chain subscriptions during publishing
40+
/// </remarks>
41+
void PublishSafe<T>(T message) where T : IMessage;
42+
3043
/// <summary>
3144
/// Subscribes to the message type.
3245
/// Will invoke the <paramref name="action"/> every time the message of the subscribed type is published.
3346
/// </summary>
3447
void Subscribe<T>(Action<T> action) where T : IMessage;
35-
36-
/// <summary>
37-
/// Unsubscribe the <paramref name="action"/> from the message broker.
38-
/// </summary>
39-
void Unsubscribe<T>(Action<T> action) where T : IMessage;
40-
48+
4149
/// <summary>
42-
/// Unsubscribe all actions from the message broker from of the given message type.
50+
/// Unsubscribe the action of <typeparamref name="T"/> from the <paramref name="subscriber"/> in the message broker.
51+
/// If <paramref name="subscriber"/> is null then will unsubscribe from ALL subscribers currently subscribed to <typeparamref name="T"/>
4352
/// </summary>
44-
void Unsubscribe<T>() where T : IMessage;
53+
void Unsubscribe<T>(object subscriber = null) where T : IMessage;
4554

4655
/// <summary>
4756
/// Unsubscribe from all messages.
@@ -53,7 +62,9 @@ public interface IMessageBrokerService
5362
/// <inheritdoc />
5463
public class MessageBrokerService : IMessageBrokerService
5564
{
56-
private readonly IDictionary<Type, IDictionary<object, IList>> _subscriptions = new Dictionary<Type, IDictionary<object, IList>>();
65+
private readonly IDictionary<Type, IDictionary<object, object>> _subscriptions = new Dictionary<Type, IDictionary<object, object>>();
66+
67+
private (bool, IMessage) _isPublishing;
5768

5869
/// <inheritdoc />
5970
public void Publish<T>(T message) where T : IMessage
@@ -63,18 +74,35 @@ public void Publish<T>(T message) where T : IMessage
6374
return;
6475
}
6576

66-
var subscriptionCopy = new IList[subscriptionObjects.Count];
67-
77+
_isPublishing = (true, message);
78+
79+
foreach (var subscription in subscriptionObjects)
80+
{
81+
var action = (Action<T>)subscription.Value;
82+
83+
action(message);
84+
}
85+
86+
_isPublishing = (false, message);
87+
}
88+
89+
/// <inheritdoc />
90+
public void PublishSafe<T>(T message) where T : IMessage
91+
{
92+
if (!_subscriptions.TryGetValue(typeof(T), out var subscriptionObjects))
93+
{
94+
return;
95+
}
96+
97+
var subscriptionCopy = new object[subscriptionObjects.Count];
98+
6899
subscriptionObjects.Values.CopyTo(subscriptionCopy, 0);
69100

70101
for (var i = 0; i < subscriptionCopy.Length; i++)
71102
{
72-
var actions = (List<Action<T>>) subscriptionCopy[i];
103+
var action = (Action<T>)subscriptionCopy[i];
73104

74-
for (var index = 0; index < actions.Count; index++)
75-
{
76-
actions[index](message);
77-
}
105+
action(message);
78106
}
79107
}
80108

@@ -88,58 +116,51 @@ public void Subscribe<T>(Action<T> action) where T : IMessage
88116
{
89117
throw new ArgumentException("Subscribe static functions to a message is not supported!");
90118
}
91-
92-
if (!_subscriptions.TryGetValue(type, out var subscriptionObjects))
119+
if(_isPublishing.Item1)
93120
{
94-
subscriptionObjects = new Dictionary<object, IList>();
95-
_subscriptions.Add(type, subscriptionObjects);
121+
throw new InvalidOperationException($"Cannot subscribe to {type.Name} message while publishing " +
122+
$"{_isPublishing.Item2.GetType().Name} message. Use {nameof(PublishSafe)} instead!");
96123
}
97124

98-
if (!subscriptionObjects.TryGetValue(subscriber, out IList actions))
125+
if (!_subscriptions.TryGetValue(type, out var subscriptionObjects))
99126
{
100-
actions = new List<Action<T>>();
101-
subscriptionObjects.Add(subscriber, actions);
127+
subscriptionObjects = new Dictionary<object, object>();
128+
_subscriptions.Add(type, subscriptionObjects);
102129
}
103130

104-
actions.Add(action);
131+
subscriptionObjects[subscriber] = action;
105132
}
106133

107134
/// <inheritdoc />
108-
public void Unsubscribe<T>(Action<T> action) where T : IMessage
135+
public void Unsubscribe<T>(object subscriber = null) where T : IMessage
109136
{
110137
var type = typeof(T);
111-
var subscriber = action.Target;
112138

113139
if (subscriber == null)
114140
{
115-
throw new ArgumentException("Subscribe static functions to a message is not supported!");
116-
}
141+
_subscriptions.Remove(type);
117142

118-
if (!_subscriptions.TryGetValue(type, out var subscriptionObjects) ||
119-
!subscriptionObjects.TryGetValue(subscriber, out var actions))
120-
{
121143
return;
122144
}
123145

124-
actions.Remove(action);
125-
126-
if (actions.Count == 0)
146+
if (_isPublishing.Item1)
127147
{
128-
subscriptionObjects.Remove(subscriber);
148+
throw new InvalidOperationException($"Cannot unsubscribe to {type.Name} message while publishing " +
149+
$"{_isPublishing.Item2.GetType().Name} message. Use {nameof(PublishSafe)} instead!");
150+
}
151+
if (!_subscriptions.TryGetValue(type, out var subscriptionObjects))
152+
{
153+
return;
129154
}
130155

156+
subscriptionObjects.Remove(subscriber);
157+
131158
if (subscriptionObjects.Count == 0)
132159
{
133160
_subscriptions.Remove(type);
134161
}
135162
}
136163

137-
/// <inheritdoc />
138-
public void Unsubscribe<T>() where T : IMessage
139-
{
140-
_subscriptions.Remove(typeof(T));
141-
}
142-
143164
/// <inheritdoc />
144165
public void UnsubscribeAll(object subscriber = null)
145166
{
@@ -151,10 +172,7 @@ public void UnsubscribeAll(object subscriber = null)
151172

152173
foreach (var subscriptionObjects in _subscriptions.Values)
153174
{
154-
if (subscriptionObjects.ContainsKey(subscriber))
155-
{
156-
subscriptionObjects.Remove(subscriber);
157-
}
175+
subscriptionObjects.Remove(subscriber);
158176
}
159177
}
160178
}
File renamed without changes.

Tests/Editor/GameLovers.Services.Tests.asmdef.meta renamed to Tests/Editor/EditMode/GameLovers.Services.Tests.asmdef.meta

File renamed without changes.

Tests/Editor/EditMode/InstallerTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
using System;
2+
using GameLovers.Services;
23
using NUnit.Framework;
34

45
// ReSharper disable once CheckNamespace
56

6-
namespace GameLovers.Services.Tests
7+
namespace GameLoversEditor.Services.Tests
78
{
89
public class InstallerTest
910
{

Tests/Editor/EditMode/MessageBrokerServiceTest.cs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,35 @@ public void Subscribe_Publish_Successfully()
3838
{
3939
_messageBroker.Subscribe<MessageType1>(_subscriber.MockMessageCall);
4040
_messageBroker.Publish(_messageType1);
41+
_messageBroker.PublishSafe(_messageType1);
4142

42-
_subscriber.Received().MockMessageCall(_messageType1);
43+
_subscriber.Received(2).MockMessageCall(_messageType1);
44+
}
45+
46+
[Test]
47+
public void Subscribe_MultipleSubscriptionSameType_ReplacePreviousSubscription()
48+
{
49+
_messageBroker.Subscribe<MessageType1>(_subscriber.MockMessageCall);
50+
_messageBroker.Subscribe<MessageType1>(_subscriber.MockMessageCall2);
51+
_messageBroker.Publish(_messageType1);
52+
_messageBroker.PublishSafe(_messageType1);
53+
54+
_subscriber.DidNotReceive().MockMessageCall(_messageType1);
55+
_subscriber.Received(2).MockMessageCall2(_messageType1);
56+
}
57+
58+
[Test]
59+
public void Publish_ChainSubscribe_Successfully()
60+
{
61+
// TODO: Test
62+
Assert.True(true);
4363
}
4464

4565
[Test]
4666
public void Publish_WithoutSubscription_DoesNothing()
4767
{
4868
_messageBroker.Publish(_messageType1);
69+
_messageBroker.PublishSafe(_messageType1);
4970

5071
_subscriber.DidNotReceive().MockMessageCall(_messageType1);
5172
}
@@ -54,22 +75,24 @@ public void Publish_WithoutSubscription_DoesNothing()
5475
public void Unsubscribe_Successfully()
5576
{
5677
_messageBroker.Subscribe<MessageType1>(_subscriber.MockMessageCall);
57-
_messageBroker.Unsubscribe<MessageType1>(_subscriber.MockMessageCall);
78+
_messageBroker.Unsubscribe<MessageType1>(_subscriber);
5879
_messageBroker.Publish(_messageType1);
80+
_messageBroker.PublishSafe(_messageType1);
5981

6082
_subscriber.DidNotReceive().MockMessageCall(_messageType1);
6183
}
6284

6385
[Test]
64-
public void UnsubscribeWithAction_KeepsSubscriptionSameType_Successfully()
86+
public void UnsubscribeWithAction_MultipleSubscriptionSameType_RemoveAllScriptionsOfSameType()
6587
{
6688
_messageBroker.Subscribe<MessageType1>(_subscriber.MockMessageCall);
6789
_messageBroker.Subscribe<MessageType1>(_subscriber.MockMessageCall2);
68-
_messageBroker.Unsubscribe<MessageType1>(_subscriber.MockMessageCall);
90+
_messageBroker.Unsubscribe<MessageType1>(_subscriber);
6991
_messageBroker.Publish(_messageType1);
92+
_messageBroker.PublishSafe(_messageType1);
7093

7194
_subscriber.DidNotReceive().MockMessageCall(_messageType1);
72-
_subscriber.Received().MockMessageCall2(_messageType1);
95+
_subscriber.DidNotReceive().MockMessageCall2(_messageType1);
7396
}
7497

7598
[Test]
@@ -79,9 +102,10 @@ public void UnsubscribeWithoutAction_KeepsSubscriptionDifferentType_Successfully
79102
_messageBroker.Subscribe<MessageType2>(_subscriber.MockMessageAlternativeCall);
80103
_messageBroker.Unsubscribe<MessageType1>();
81104
_messageBroker.Publish(_messageType2);
105+
_messageBroker.PublishSafe(_messageType2);
82106

83107
_subscriber.DidNotReceive().MockMessageCall(_messageType1);
84-
_subscriber.Received().MockMessageAlternativeCall(_messageType2);
108+
_subscriber.Received(2).MockMessageAlternativeCall(_messageType2);
85109
}
86110

87111
[Test]
@@ -92,8 +116,10 @@ public void UnsubscribeAll_Successfully()
92116
_messageBroker.Subscribe<MessageType2>(_subscriber.MockMessageAlternativeCall);
93117
_messageBroker.Subscribe<MessageType2>(_subscriber.MockMessageAlternativeCall2);
94118
_messageBroker.UnsubscribeAll();
119+
_messageBroker.Publish(_messageType1);
95120
_messageBroker.Publish(_messageType2);
96-
_messageBroker.Publish(_messageType2);
121+
_messageBroker.PublishSafe(_messageType1);
122+
_messageBroker.PublishSafe(_messageType2);
97123

98124
_subscriber.DidNotReceive().MockMessageCall(_messageType1);
99125
_subscriber.DidNotReceive().MockMessageCall2(_messageType1);
@@ -104,7 +130,7 @@ public void UnsubscribeAll_Successfully()
104130
[Test]
105131
public void Unsubscribe_WithoutSubscription_DoesNothing()
106132
{
107-
Assert.DoesNotThrow(() => _messageBroker.Unsubscribe<MessageType1>(_subscriber.MockMessageCall));
133+
Assert.DoesNotThrow(() => _messageBroker.Unsubscribe<MessageType1>(_subscriber));
108134
Assert.DoesNotThrow(() => _messageBroker.Unsubscribe<MessageType1>());
109135
Assert.DoesNotThrow(() => _messageBroker.UnsubscribeAll());
110136
}

Tests/Editor/PlayMode/GameLovers.Services.Tests.Playmode.asmdef

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
"name": "GameLovers.Services.Tests.Playmode",
33
"rootNamespace": "",
44
"references": [
5-
"GUID:27619889b8ba8c24980f49ee34dbb44a",
6-
"GUID:15a85301a6cee40849303ad50f7a0322"
5+
"UnityEngine.TestRunner",
6+
"UnityEditor.TestRunner",
7+
"GameLovers.Services"
78
],
89
"includePlatforms": [],
910
"excludePlatforms": [],
@@ -13,7 +14,9 @@
1314
"nunit.framework.dll"
1415
],
1516
"autoReferenced": true,
16-
"defineConstraints": [],
17+
"defineConstraints": [
18+
"UNITY_INCLUDE_TESTS"
19+
],
1720
"versionDefines": [],
1821
"noEngineReferences": false
1922
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "com.gamelovers.services",
33
"displayName": "Services",
44
"author": "Miguel Tomas",
5-
"version": "0.13.2",
5+
"version": "0.14.0",
66
"unity": "2022.3",
77
"license": "MIT",
88
"description": "The purpose of this package is to provide a set of services to ease the development of a basic game architecture",

0 commit comments

Comments
 (0)