diff --git a/Directory.Build.props b/Directory.Build.props
index 3c84b8a5..5cc68296 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,7 +14,7 @@
MIT
© Meta Vens and contributors
true
- 1.0.17
+ 1.0.18
true
diff --git a/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs b/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs
index 1597be2a..77528970 100644
--- a/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs
+++ b/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs
@@ -75,6 +75,9 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
foreach (var pair in loaded)
{
+ if (_dirtyKeys.ContainsKey(pair.Key) || _deletedKeys.ContainsKey(pair.Key))
+ continue;
+
if (Entries.TryGetValue(pair.Key, out var existing) &&
string.Equals(existing.StoredAt, pair.Value.StoredAt, StringComparison.Ordinal) &&
string.Equals(existing.Descriptor.SchemaId, pair.Value.Descriptor.SchemaId, StringComparison.Ordinal))
@@ -91,14 +94,15 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
foreach (var removedKey in Entries.Keys.Where(key => !loaded.ContainsKey(key)).ToArray())
{
+ if (_dirtyKeys.ContainsKey(removedKey) || _deletedKeys.ContainsKey(removedKey))
+ continue;
+
if (Entries.TryRemove(removedKey, out var removed))
EntryDeleted.OnNext(removed);
}
SetLastSchemaMigrationReports(reports);
- _dirtyKeys.Clear();
- _deletedKeys.Clear();
- IsDirty = false;
+ IsDirty = !_dirtyKeys.IsEmpty || !_deletedKeys.IsEmpty;
}
///
diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs
index ba9bdbdf..b06391be 100644
--- a/src/GameCult.Caching/CultCache.cs
+++ b/src/GameCult.Caching/CultCache.cs
@@ -2092,6 +2092,13 @@ namespace GameCult.Caching
return;
}
+ // A single-file snapshot cannot distinguish local dirty keys from clean keys.
+ // Pulling while local mutations are staged would let an older disk snapshot
+ // erase those mutations before the next flush. Flush first; clean readers can
+ // still poll external snapshots incrementally.
+ if (IsDirty)
+ return;
+
CultPersistedStoreSnapshot snapshot;
try
{
diff --git a/tests/GameCult.Caching.Tests/BackingStoreTests.cs b/tests/GameCult.Caching.Tests/BackingStoreTests.cs
index 328b778e..9e9a7606 100644
--- a/tests/GameCult.Caching.Tests/BackingStoreTests.cs
+++ b/tests/GameCult.Caching.Tests/BackingStoreTests.cs
@@ -107,6 +107,53 @@ namespace GameCult.Caching.Tests
}
}
+ [Test]
+ public async Task SingleFileMessagePackBackingStore_PullAll_DoesNotEraseUnflushedLocalMutations()
+ {
+ var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.msgpack");
+
+ try
+ {
+ using (var seed = await CultCacheMessagePack.OpenAsync(filePath))
+ {
+ await seed.UpsertAsync(new NamedTestEntry
+ {
+ Name = "persisted",
+ Value = "before-local-write"
+ });
+ await seed.FlushAsync();
+ }
+
+ CultRecordHandle localHandle;
+ using (var cache = await CultCacheMessagePack.OpenAsync(filePath))
+ {
+ localHandle = await cache.UpsertAsync(new NamedTestEntry
+ {
+ Name = "local",
+ Value = "must-survive-pull"
+ });
+
+ Assert.That(cache.IsDirty, Is.True);
+ await cache.PullAllBackingStoresAsync();
+
+ Assert.That(cache.Get(localHandle.Key)?.Value,
+ Is.EqualTo("must-survive-pull"));
+ Assert.That(cache.IsDirty, Is.True,
+ "pulling a clean disk snapshot must not pardon staged local mutations");
+ await cache.FlushAsync();
+ }
+
+ using var reopened = await CultCacheMessagePack.OpenAsync(filePath);
+ Assert.That(reopened.Get(localHandle.Key)?.Value,
+ Is.EqualTo("must-survive-pull"));
+ }
+ finally
+ {
+ if (File.Exists(filePath))
+ File.Delete(filePath);
+ }
+ }
+
[Test]
public async Task CultCache_DirtyState_Tracks_Mutations_And_ExplicitFlush()
{
@@ -425,6 +472,60 @@ namespace GameCult.Caching.Tests
}
}
+ [Test]
+ public async Task DirectoryMessagePackBackingStore_PullAll_PreservesUnflushedLocalKeys()
+ {
+ var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.cc");
+ var recordsPath = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath);
+
+ try
+ {
+ using (var seed = await CultCacheMessagePack.OpenAsync(
+ filePath,
+ new CultCacheOpenOptions { UseDirectoryStore = true }))
+ {
+ await seed.UpsertAsync(new NamedTestEntry
+ {
+ Name = "persisted",
+ Value = "before-local-write"
+ });
+ await seed.FlushAsync();
+ }
+
+ CultRecordHandle localHandle;
+ using (var cache = await CultCacheMessagePack.OpenAsync(
+ filePath,
+ new CultCacheOpenOptions { UseDirectoryStore = true }))
+ {
+ localHandle = await cache.UpsertAsync(new NamedTestEntry
+ {
+ Name = "local",
+ Value = "must-survive-pull"
+ });
+
+ await cache.PullAllBackingStoresAsync();
+
+ Assert.That(cache.Get(localHandle.Key)?.Value,
+ Is.EqualTo("must-survive-pull"));
+ Assert.That(cache.IsDirty, Is.True);
+ await cache.FlushAsync();
+ }
+
+ using var reopened = await CultCacheMessagePack.OpenAsync(
+ filePath,
+ new CultCacheOpenOptions { UseDirectoryStore = true });
+ Assert.That(reopened.Get(localHandle.Key)?.Value,
+ Is.EqualTo("must-survive-pull"));
+ }
+ finally
+ {
+ if (File.Exists(filePath))
+ File.Delete(filePath);
+ if (Directory.Exists(recordsPath))
+ Directory.Delete(recordsPath, recursive: true);
+ }
+ }
+
[Test]
public async Task DirectoryMessagePackBackingStore_Remains_Dirty_When_Deletion_Fails()
{
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll
index a04121ee..8786becc 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb
index 52e90dd3..20d98383 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll
index 13dda8a4..558b6852 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb
index e5fa0f25..2c5a3cae 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll
index bbb19751..aa345f22 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb
index 48df96f5..a4db4304 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll
index 4c209fbe..551c17e8 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb
index 23a5cc92..88694cb6 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll
index 9d8d79cb..551dbfcb 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb
index 4175bf71..41c757da 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb differ
diff --git a/unity/org.gamecult.cultlib/package.json b/unity/org.gamecult.cultlib/package.json
index 66fcfa90..c235eb67 100644
--- a/unity/org.gamecult.cultlib/package.json
+++ b/unity/org.gamecult.cultlib/package.json
@@ -1,7 +1,7 @@
{
"name": "org.gamecult.cultlib",
"displayName": "CultLib",
- "version": "1.0.17",
+ "version": "1.0.18",
"unity": "2021.3",
"description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.",
"license": "MIT",