From 888f472f699780aadd59d8a82f30d96e699059c1 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Sun, 17 May 2026 07:02:49 -0500 Subject: [PATCH] Introduce pluggable snapshot storage for persistence --- .../Persistence/ISnapshotStore.cs | 7 ++++ .../Persistence/InMemoryCloudStateStore.cs | 9 +++- .../InMemoryPersonalMemoryStore.cs | 9 +++- ...pshotStore.cs => JsonFileSnapshotStore.cs} | 8 ++-- .../Infrastructure/PersistenceStoreTests.cs | 42 +++++++++++++++++++ 5 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/ISnapshotStore.cs rename OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/{JsonSnapshotStore.cs => JsonFileSnapshotStore.cs} (77%) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/ISnapshotStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/ISnapshotStore.cs new file mode 100644 index 0000000..af312c9 --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/ISnapshotStore.cs @@ -0,0 +1,7 @@ +namespace Jibo.Cloud.Infrastructure.Persistence; + +public interface ISnapshotStore +{ + TSnapshot? Load() where TSnapshot : class; + void Save(TSnapshot snapshot) where TSnapshot : class; +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs index 2ad0b5d..f0c4737 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs @@ -18,7 +18,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore private readonly ConcurrentDictionary _sessionsByToken = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _symmetricKeys = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _keyRequests = new(StringComparer.OrdinalIgnoreCase); - private readonly JsonSnapshotStore _snapshotStore; + private readonly ISnapshotStore _snapshotStore; private readonly Lock _syncRoot = new(); private readonly List _updates; private readonly List _media = []; @@ -32,8 +32,13 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore private DateTimeOffset? _lastSavedUtc; public InMemoryCloudStateStore(string? persistencePath = null) + : this(new JsonFileSnapshotStore(persistencePath, PersistenceJsonOptions)) { - _snapshotStore = new JsonSnapshotStore(persistencePath, PersistenceJsonOptions); + } + + public InMemoryCloudStateStore(ISnapshotStore snapshotStore) + { + _snapshotStore = snapshotStore; _robot = new DeviceRegistration { HostMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryPersonalMemoryStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryPersonalMemoryStore.cs index 18f4e23..ccc6dd9 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryPersonalMemoryStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryPersonalMemoryStore.cs @@ -13,15 +13,20 @@ public sealed class InMemoryPersonalMemoryStore : IPersonalMemoryStore }; private readonly ConcurrentDictionary _tenantMemory = new(StringComparer.OrdinalIgnoreCase); - private readonly JsonSnapshotStore _snapshotStore; + private readonly ISnapshotStore _snapshotStore; private readonly Lock _syncRoot = new(); private long _revision; private DateTimeOffset? _lastLoadedUtc; private DateTimeOffset? _lastSavedUtc; public InMemoryPersonalMemoryStore(string? persistencePath = null) + : this(new JsonFileSnapshotStore(persistencePath, PersistenceJsonOptions)) { - _snapshotStore = new JsonSnapshotStore(persistencePath, PersistenceJsonOptions); + } + + public InMemoryPersonalMemoryStore(ISnapshotStore snapshotStore) + { + _snapshotStore = snapshotStore; LoadPersistedState(); } diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/JsonSnapshotStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/JsonFileSnapshotStore.cs similarity index 77% rename from OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/JsonSnapshotStore.cs rename to OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/JsonFileSnapshotStore.cs index c20fb74..e982eaf 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/JsonSnapshotStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/JsonFileSnapshotStore.cs @@ -2,18 +2,18 @@ using System.Text.Json; namespace Jibo.Cloud.Infrastructure.Persistence; -internal sealed class JsonSnapshotStore +internal sealed class JsonFileSnapshotStore : ISnapshotStore { private readonly string? _persistencePath; private readonly JsonSerializerOptions _options; - public JsonSnapshotStore(string? persistencePath, JsonSerializerOptions options) + public JsonFileSnapshotStore(string? persistencePath, JsonSerializerOptions options) { _persistencePath = persistencePath; _options = options; } - public TSnapshot? Load() + public TSnapshot? Load() where TSnapshot : class { if (string.IsNullOrWhiteSpace(_persistencePath) || !File.Exists(_persistencePath)) { @@ -30,7 +30,7 @@ internal sealed class JsonSnapshotStore } } - public void Save(TSnapshot snapshot) + public void Save(TSnapshot snapshot) where TSnapshot : class { if (string.IsNullOrWhiteSpace(_persistencePath)) { diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Infrastructure/PersistenceStoreTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Infrastructure/PersistenceStoreTests.cs index 10bc26a..f25b6a8 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Infrastructure/PersistenceStoreTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Infrastructure/PersistenceStoreTests.cs @@ -5,6 +5,33 @@ namespace Jibo.Cloud.Tests.Infrastructure; public sealed class PersistenceStoreTests { + [Fact] + public void PersonalMemoryStore_CanUseAlternateSnapshotBackend() + { + var backend = new RecordingSnapshotStore(); + var store = new InMemoryPersonalMemoryStore(backend); + var scope = new PersonalMemoryTenantScope("acct-b", "loop-b", "device-b", "person-b"); + + store.SetName(scope, "Alt Backend"); + + Assert.Single(backend.Saves); + Assert.Equal("Alt Backend", store.GetName(scope)); + Assert.Equal("1", store.GetPersistenceStateInfo().SchemaVersion); + } + + [Fact] + public void CloudStateStore_CanUseAlternateSnapshotBackend() + { + var backend = new RecordingSnapshotStore(); + var store = new InMemoryCloudStateStore(backend); + + store.CreateMedia("openjibo-default-loop", "backend-photo", "image", "photo-ref", false, null); + + Assert.Single(backend.Saves); + Assert.Contains(store.ListMedia(), item => item.Path == "backend-photo"); + Assert.Equal("1", store.GetPersistenceStateInfo().SchemaVersion); + } + [Fact] public void PersonalMemoryStore_RoundTripsStateAndRevision() { @@ -84,4 +111,19 @@ public sealed class PersistenceStoreTests } } } + + private sealed class RecordingSnapshotStore : ISnapshotStore + { + public List Saves { get; } = []; + + public TSnapshot2? Load() where TSnapshot2 : class + { + return default; + } + + public void Save(TSnapshot2 snapshot) where TSnapshot2 : class + { + Saves.Add(snapshot); + } + } }