mirror of https://github.com/Squidex/squidex.git
19 changed files with 449 additions and 140 deletions
@ -0,0 +1,133 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.ObjectPool; |
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing |
|||
{ |
|||
public sealed class RecentEvents |
|||
{ |
|||
private readonly HashSet<Guid> eventIds; |
|||
private readonly Queue<(Guid, string)> eventQueue; |
|||
private readonly int capacity; |
|||
|
|||
public IEnumerable<(Guid, string)> EventQueue => eventQueue; |
|||
|
|||
public RecentEvents(int capacity = 50) |
|||
{ |
|||
this.capacity = capacity; |
|||
|
|||
eventIds = new HashSet<Guid>(capacity); |
|||
eventQueue = new Queue<(Guid, string)>(capacity); |
|||
} |
|||
|
|||
public string? FirstPosition() |
|||
{ |
|||
if (eventQueue.Count == 0) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return eventQueue.Peek().Item2; |
|||
} |
|||
|
|||
public bool Add(StoredEvent @event) |
|||
{ |
|||
return Add(@event.Data.Headers.EventId(), @event.EventPosition); |
|||
} |
|||
|
|||
public bool Add(Guid id, string position) |
|||
{ |
|||
if (eventIds.Contains(id)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
while (eventQueue.Count >= capacity) |
|||
{ |
|||
var (storedId, _) = eventQueue.Dequeue(); |
|||
|
|||
eventIds.Remove(storedId); |
|||
} |
|||
|
|||
eventIds.Add(id); |
|||
eventQueue.Enqueue((id, position)); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public static RecentEvents Parse(string? input) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(input)) |
|||
{ |
|||
return new RecentEvents(); |
|||
} |
|||
|
|||
return Parse(input.AsSpan()); |
|||
} |
|||
|
|||
private static RecentEvents Parse(ReadOnlySpan<char> span) |
|||
{ |
|||
var result = new RecentEvents(); |
|||
|
|||
while (span.Length > 0) |
|||
{ |
|||
var endOfLine = span.IndexOf('\n'); |
|||
|
|||
if (endOfLine < 0) |
|||
{ |
|||
endOfLine = span.Length - 1; |
|||
} |
|||
|
|||
var line = span[0..endOfLine]; |
|||
|
|||
var separator = line.IndexOf('|'); |
|||
|
|||
if (separator > 0 && separator < line.Length - 1) |
|||
{ |
|||
var guidSpan = line[0..separator]; |
|||
|
|||
if (Guid.TryParse(guidSpan, out var id)) |
|||
{ |
|||
result.Add(id, line[(separator + 1)..].ToString()); |
|||
} |
|||
} |
|||
|
|||
span = span[endOfLine..]; |
|||
span = span.TrimStart('\n'); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public override string? ToString() |
|||
{ |
|||
if (eventQueue.Count == 0) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var sb = DefaultPools.StringBuilder.Get(); |
|||
try |
|||
{ |
|||
foreach (var (id, position) in eventQueue) |
|||
{ |
|||
sb.Append(id); |
|||
sb.Append('|'); |
|||
sb.Append(position); |
|||
sb.Append('\n'); |
|||
} |
|||
|
|||
return sb.ToString(); |
|||
} |
|||
finally |
|||
{ |
|||
DefaultPools.StringBuilder.Return(sb); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing |
|||
{ |
|||
public record struct SubscriptionQuery |
|||
{ |
|||
public string? Position { get; set; } |
|||
|
|||
public string? StreamFilter { get; set; } |
|||
|
|||
public Dictionary<string, string>? Context { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Globalization; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing |
|||
{ |
|||
public sealed class RecentEventsTests |
|||
{ |
|||
[Fact] |
|||
public void Should_add_events() |
|||
{ |
|||
var event1 = CreateEvent(0); |
|||
var event2 = CreateEvent(0); |
|||
var event3 = CreateEvent(0); |
|||
|
|||
var sut = new RecentEvents(3); |
|||
|
|||
sut.Add(event1.Id, event1.Position); |
|||
sut.Add(event2.Id, event2.Position); |
|||
sut.Add(event3.Id, event3.Position); |
|||
|
|||
Assert.Equal(sut.EventQueue.ToArray(), new[] { event1, event2, event3 }); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_remove_old_events_when_capacity_reached() |
|||
{ |
|||
var event1 = CreateEvent(0); |
|||
var event2 = CreateEvent(0); |
|||
var event3 = CreateEvent(0); |
|||
|
|||
var sut = new RecentEvents(2); |
|||
|
|||
sut.Add(event1.Id, event1.Position); |
|||
sut.Add(event2.Id, event2.Position); |
|||
sut.Add(event3.Id, event3.Position); |
|||
|
|||
Assert.Equal(sut.EventQueue.ToArray(), new[] { event2, event3 }); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_add_events_twice() |
|||
{ |
|||
var event1 = CreateEvent(0); |
|||
var event2 = CreateEvent(0); |
|||
|
|||
var sut = new RecentEvents(2); |
|||
|
|||
var added1 = sut.Add(event1.Id, event1.Position); |
|||
var added2 = sut.Add(event2.Id, event2.Position); |
|||
var added3 = sut.Add(event2.Id, event2.Position); |
|||
|
|||
Assert.Equal(sut.EventQueue.ToArray(), new[] { event1, event2 }); |
|||
Assert.True(added1); |
|||
Assert.True(added2); |
|||
Assert.False(added3); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0)] |
|||
[InlineData(10)] |
|||
[InlineData(50)] |
|||
[InlineData(100)] |
|||
public void Should_serialize_and_deserialize(int count) |
|||
{ |
|||
var source = new RecentEvents(); |
|||
|
|||
for (var i = 0; i < count; i++) |
|||
{ |
|||
var @event = CreateEvent(i); |
|||
|
|||
source.Add(@event.Id, @event.Position); |
|||
} |
|||
|
|||
var serialized = RecentEvents.Parse(source.ToString()); |
|||
|
|||
Assert.Equal(source.EventQueue.ToArray(), serialized.EventQueue.ToArray()); |
|||
} |
|||
|
|||
private static (Guid Id, string Position) CreateEvent(int position) |
|||
{ |
|||
return (Guid.NewGuid(), position.ToString(CultureInfo.InvariantCulture)); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue