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