< Summary

Information
Class: Elsa.AI.Host.Services.InMemoryAIConversationStore
Assembly: Elsa.AI.Host
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.AI.Host/Services/InMemoryAIConversationStore.cs
Line coverage
92%
Covered lines: 35
Uncovered lines: 3
Coverable lines: 38
Total lines: 76
Line coverage: 92.1%
Branch coverage
85%
Covered branches: 24
Total branches: 28
Branch coverage: 85.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
FindAsync(...)100%44100%
SaveAsync(...)100%11100%
PruneExpired()50%2266.66%
IsExpired(...)80%101083.33%
ValidateOwnership(...)100%66100%
NormalizeTenantId(...)100%22100%
Validate(...)75%4480%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.AI.Host/Services/InMemoryAIConversationStore.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using Elsa.AI.Abstractions.Contracts;
 3using Elsa.AI.Abstractions.Models;
 4
 5namespace Elsa.AI.Host.Services;
 6
 7public class InMemoryAIConversationStore : IAITransientConversationStore
 8{
 489    private readonly ConcurrentDictionary<string, AIConversation> _conversations = new(StringComparer.OrdinalIgnoreCase)
 10
 11    public ValueTask<AIConversation?> FindAsync(string id, CancellationToken cancellationToken = default)
 12    {
 5813        _conversations.TryGetValue(id, out var conversation);
 5814        if (conversation == null || !IsExpired(conversation))
 5615            return ValueTask.FromResult(conversation);
 16
 217        _conversations.TryRemove(id, out _);
 218        conversation = null;
 19
 220        return ValueTask.FromResult(conversation);
 21    }
 22
 23    public ValueTask SaveAsync(AIConversation conversation, CancellationToken cancellationToken = default)
 24    {
 10925        Validate(conversation);
 10826        PruneExpired();
 10827        _conversations.AddOrUpdate(
 10828            conversation.Id,
 10829            conversation,
 10830            (_, existing) =>
 10831            {
 6132                ValidateOwnership(existing, conversation);
 5933                return conversation;
 10834            });
 35
 10636        return ValueTask.CompletedTask;
 37    }
 38
 39    private void PruneExpired()
 40    {
 21641        foreach (var conversation in _conversations.Values.Where(IsExpired))
 042            _conversations.TryRemove(conversation.Id, out _);
 10843    }
 44
 45    private bool IsExpired(AIConversation conversation)
 46    {
 9347        if (conversation.RetentionMode == AIRetentionMode.Ephemeral)
 248            return conversation.Status is AIConversationStatus.Completed or AIConversationStatus.Failed;
 49
 9150        if (conversation.RetentionMode == AIRetentionMode.Durable)
 051            return false;
 52
 9153        var expiresAt = conversation.RetentionExpiresAt;
 9154        return expiresAt.HasValue && expiresAt <= DateTimeOffset.UtcNow;
 55    }
 56
 57    private static void ValidateOwnership(AIConversation existing, AIConversation conversation)
 58    {
 6159        if (!string.Equals(NormalizeTenantId(existing.TenantId), NormalizeTenantId(conversation.TenantId), StringCompari
 160            throw new InvalidOperationException("Cannot overwrite an AI conversation that belongs to another tenant.");
 61
 6062        if (!string.IsNullOrWhiteSpace(existing.UserId) && !string.Equals(existing.UserId, conversation.UserId, StringCo
 163            throw new InvalidOperationException("Cannot overwrite an AI conversation that belongs to another user.");
 5964    }
 65
 12266    private static string NormalizeTenantId(string? tenantId) => tenantId ?? "";
 67
 68    private static void Validate(AIConversation conversation)
 69    {
 10970        if (string.IsNullOrWhiteSpace(conversation.Id))
 071            throw new ArgumentException("A conversation ID is required.", nameof(conversation));
 72
 10973        if (string.IsNullOrWhiteSpace(conversation.UserId))
 174            throw new ArgumentException("A conversation user ID is required.", nameof(conversation));
 10875    }
 76}