Skip to content

Commit 93f8aa6

Browse files
authored
feat: AutoChannel and duplicate filtering for AutoCache
co-author: @Bastani
2 parents 84e869d + 7b00088 commit 93f8aa6

File tree

8 files changed

+542
-2
lines changed

8 files changed

+542
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ bin/
66
obj/
77
.generated/
88
.vs/
9+
.idea/
910
.DS_Store
1011
*.DotSettings.user
1112
*.binlog

Chickensoft.Sync.Tests/src/SyncSubjectTest.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,24 @@ public sealed class TestOwner<T> : IPerform<T> where T : struct
1717
public void Perform(in T op) => Action(Subject, op);
1818
}
1919

20+
public sealed class TestOwnerAny :
21+
IPerformAnyOperation,
22+
IPerform<TestOwnerAny.TestOp1>,
23+
IPerform<TestOwnerAny.TestOp2>
24+
{
25+
public readonly record struct TestOp1;
26+
public readonly record struct TestOp2;
27+
public SyncSubject Subject { get; set; } = default!;
28+
29+
public required Action<SyncSubject, object> Action { get; set; }
30+
public required Action<SyncSubject, TestOp1> TestAction1 { get; init; }
31+
public required Action<SyncSubject, TestOp2> TestAction2 { get; init; }
32+
33+
public void Perform<TOp>(in TOp op) where TOp : struct => Action(Subject, op);
34+
public void Perform(in TestOp1 op) => TestAction1(Subject, op);
35+
public void Perform(in TestOp2 op) => TestAction2(Subject, op);
36+
}
37+
2038
public TestOwner<int> Nop => new()
2139
{
2240
Action = (_, __) => { }
@@ -232,6 +250,78 @@ public void PerformsOpsSerialized()
232250
log.ShouldBe(["owner 1", "callback 1", "owner 2", "callback 2"]);
233251
}
234252

253+
[Fact]
254+
public void PerformsAnyOpsSerialized()
255+
{
256+
var log = new List<string>();
257+
258+
var owner = new TestOwnerAny
259+
{
260+
Action = (subj, value) =>
261+
{
262+
log.Add($"anyAction {value}");
263+
if (value is int number)
264+
subj.Broadcast(number);
265+
},
266+
TestAction1 = (subj, value) =>
267+
{
268+
log.Add($"testAction1 {value}");
269+
subj.Broadcast(value);
270+
},
271+
TestAction2 = (subj, value) =>
272+
{
273+
log.Add($"testAction2 {value}");
274+
subj.Broadcast(value);
275+
}
276+
};
277+
278+
var subject = new SyncSubject(owner);
279+
owner.Subject = subject;
280+
281+
var binding1 = new Mock<ISyncBinding>();
282+
var binding2 = new Mock<ISyncBinding>();
283+
284+
var calls = 0;
285+
286+
binding1.Setup(b => b.InvokeCallbacks(It.Ref<TestOwnerAny.TestOp1>.IsAny))
287+
.Callback((in TestOwnerAny.TestOp1 value) =>
288+
{
289+
log.Add($"callback {value}");
290+
calls++;
291+
292+
subject.IsBusy.ShouldBeTrue();
293+
294+
if (calls == 1)
295+
{
296+
subject.Perform(new TestOwnerAny.TestOp2());
297+
// this should not be broadcast yet
298+
binding2.Verify(
299+
b2 => b2.InvokeCallbacks(It.Ref<TestOwnerAny.TestOp2>.IsAny), Times.Never
300+
);
301+
}
302+
});
303+
304+
subject.AddBinding(binding1.Object);
305+
subject.AddBinding(binding2.Object);
306+
subject.Perform(new TestOwnerAny.TestOp1());
307+
subject.Perform(2);
308+
subject.IsBusy.ShouldBeFalse();
309+
310+
binding2.Verify(b2 => b2.InvokeCallbacks(It.Ref<TestOwnerAny.TestOp1>.IsAny));
311+
binding2.Verify(b2 => b2.InvokeCallbacks(It.Ref<TestOwnerAny.TestOp2>.IsAny));
312+
binding2.Verify(b2 => b2.InvokeCallbacks(2));
313+
314+
// Order should be IPerform then IPerformAnyOperation
315+
log.ShouldBe([
316+
"testAction1 TestOp1 { }", // IPerform<TestOp1>
317+
"callback TestOp1 { }", // callback for IPerform<TestOp1>
318+
"anyAction TestOp1 { }", // IPerformAnyOperation (TestOp1)
319+
"testAction2 TestOp2 { }", //IPerform<TestOp2>
320+
"anyAction TestOp2 { }", //IPerformAnyOperation (TestOp2)
321+
"anyAction 2" //IPerformAnyOperation (int)
322+
]);
323+
}
324+
235325
[Fact]
236326
public void DisposesSerialized()
237327
{

Chickensoft.Sync.Tests/src/primitives/AutoCacheTest.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,48 @@ public void BroadcastsChanges()
4343
values.ShouldBe([5, 3.14, "hello"]);
4444
}
4545

46+
[Fact]
47+
public void DoesNotBroadcastUnchangedValues()
48+
{
49+
var cache = new AutoCache();
50+
51+
var values = new List<object>();
52+
53+
cache.Bind()
54+
.OnUpdate((in int v) => values.Add(v));
55+
56+
cache.Update(5);
57+
cache.Update(5);
58+
cache.Update(10);
59+
cache.Update(10);
60+
cache.Update(5);
61+
62+
cache.TryGetValue<int>(out var integer).ShouldBeTrue();
63+
integer.ShouldBe(5);
64+
65+
values.ShouldBe([5, 10, 5]);
66+
}
67+
68+
[Fact]
69+
public void UsesConfiguredComparer()
70+
{
71+
var cache = new AutoCache();
72+
cache.SetComparer(new AllSameComparer());
73+
74+
var values = new List<object>();
75+
cache.Bind()
76+
.OnUpdate((in int v) => values.Add(v));
77+
78+
cache.Update(5);
79+
cache.Update(10);
80+
cache.Update(3);
81+
82+
cache.TryGetValue<int>(out var integer).ShouldBeTrue();
83+
integer.ShouldBe(5);
84+
85+
values.ShouldBe([5]);
86+
}
87+
4688
[Fact]
4789
public void BindingRespectsDerivedTypes()
4890
{
@@ -230,4 +272,10 @@ public void Disposes()
230272

231273
Should.Throw<ObjectDisposedException>(() => cache.Update(2));
232274
}
275+
276+
private class AllSameComparer : IEqualityComparer<int>
277+
{
278+
public bool Equals(int x, int y) => true;
279+
public int GetHashCode(int obj) => 0;
280+
}
233281
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
namespace Chickensoft.Sync.Tests.Primitives;
2+
3+
using Shouldly;
4+
using Sync.Primitives;
5+
6+
public sealed class AutoChannelTest
7+
{
8+
private readonly record struct TestValue(int Value);
9+
private readonly record struct TestMessage(string Text);
10+
11+
[Fact]
12+
public void Initializes()
13+
{
14+
var channel = new AutoChannel();
15+
var received = false;
16+
17+
channel.Bind()
18+
.On((in int v) => received = true);
19+
20+
received.ShouldBeFalse();
21+
}
22+
23+
[Fact]
24+
public void NoReentrancy()
25+
{
26+
var channel = new AutoChannel();
27+
var inCallback = false;
28+
var reentered = false;
29+
30+
channel.Bind()
31+
.On((in int v) =>
32+
{
33+
if (inCallback)
34+
{
35+
reentered = true; // signals immediate (re-entrant) delivery
36+
}
37+
38+
inCallback = true;
39+
40+
if (v == 1)
41+
{
42+
channel.Send(2); // attempt re-entrant send
43+
}
44+
45+
inCallback = false;
46+
});
47+
48+
channel.Send(1);
49+
50+
reentered.ShouldBe(false);
51+
}
52+
53+
[Fact]
54+
public void BroadcastsAllSentValues()
55+
{
56+
var channel = new AutoChannel();
57+
var values = new List<int>();
58+
59+
channel.Bind()
60+
.On((in int v) => values.Add(v));
61+
62+
channel.Send(5);
63+
channel.Send(10);
64+
channel.Send(10);
65+
channel.Send(15);
66+
67+
values.ShouldBe([5, 10, 10, 15]);
68+
}
69+
70+
[Fact]
71+
public void BroadcastsMultipleTypes()
72+
{
73+
var channel = new AutoChannel();
74+
var values = new List<object>();
75+
76+
channel.Bind()
77+
.On((in int v) => values.Add(v))
78+
.On((in double v) => values.Add(v))
79+
.On((in TestValue v) => values.Add(v))
80+
.On((in TestMessage v) => values.Add(v));
81+
82+
channel.Send(42);
83+
channel.Send(3.14);
84+
channel.Send(new TestValue(100));
85+
channel.Send(new TestMessage("hello"));
86+
87+
values.ShouldBe([42, 3.14, new TestValue(100), new TestMessage("hello")]);
88+
}
89+
90+
91+
[Fact]
92+
public void ConditionalCallbacks()
93+
{
94+
var channel = new AutoChannel();
95+
var values = new List<int>();
96+
var evenValues = new List<int>();
97+
98+
channel.Bind()
99+
.On((in int v) => values.Add(v))
100+
.On(
101+
(in int v) => evenValues.Add(v),
102+
condition: v => v % 2 == 0
103+
);
104+
105+
channel.Send(1);
106+
channel.Send(2);
107+
channel.Send(3);
108+
channel.Send(4);
109+
channel.Send(5);
110+
111+
values.ShouldBe([1, 2, 3, 4, 5]);
112+
evenValues.ShouldBe([2, 4]);
113+
}
114+
115+
[Fact]
116+
public void MultipleBindings()
117+
{
118+
var channel = new AutoChannel();
119+
var values1 = new List<int>();
120+
var values2 = new List<int>();
121+
var values3 = new List<int>();
122+
123+
channel.Bind()
124+
.On((in int v) => values1.Add(v));
125+
126+
channel.Bind()
127+
.On((in int v) => values2.Add(v));
128+
129+
channel.Bind()
130+
.On((in int v) => values3.Add(v));
131+
132+
channel.Send(7);
133+
channel.Send(14);
134+
135+
values1.ShouldBe([7, 14]);
136+
values2.ShouldBe([7, 14]);
137+
values3.ShouldBe([7, 14]);
138+
}
139+
140+
[Fact]
141+
public void DisposedBindingStopsReceiving()
142+
{
143+
var channel = new AutoChannel();
144+
var values = new List<int>();
145+
146+
var binding = channel.Bind();
147+
binding.On((in int v) => values.Add(v));
148+
149+
channel.Send(1);
150+
values.ShouldBe([1]);
151+
152+
binding.Dispose();
153+
154+
channel.Send(2);
155+
values.ShouldBe([1]); // Should not receive the second value
156+
}
157+
158+
[Fact]
159+
public void ClearsBindings()
160+
{
161+
var channel = new AutoChannel();
162+
var values = new List<int>();
163+
164+
using var binding = channel.Bind();
165+
binding.On((in int v) => values.Add(v));
166+
167+
channel.Send(1);
168+
channel.Send(2);
169+
170+
values.ShouldBe([1, 2]);
171+
values.Clear();
172+
173+
channel.ClearBindings();
174+
channel.Send(3);
175+
values.ShouldBeEmpty();
176+
}
177+
178+
[Fact]
179+
public void Disposes()
180+
{
181+
var channel = new AutoChannel();
182+
183+
channel.Dispose();
184+
185+
Should.Throw<ObjectDisposedException>(() => channel.Send(2));
186+
}
187+
}

0 commit comments

Comments
 (0)