refactor(kv): 整理存档接线并清理迁移冗余实现
This commit is contained in:
665
Client/Assets/Scripts/Core/KV/KvSyncModels.cs
Normal file
665
Client/Assets/Scripts/Core/KV/KvSyncModels.cs
Normal file
@@ -0,0 +1,665 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Core.KV
|
||||
{
|
||||
public enum KVSyncStatus
|
||||
{
|
||||
NotLoaded,
|
||||
Ready,
|
||||
ReadFailed,
|
||||
Conflict,
|
||||
UnknownOutcome,
|
||||
Unauthorized,
|
||||
StorageError,
|
||||
Closed
|
||||
}
|
||||
public enum KVOpenError
|
||||
{
|
||||
None,
|
||||
Validation,
|
||||
Busy,
|
||||
StorageError,
|
||||
RecoveryRequired
|
||||
}
|
||||
public enum KVChangeError
|
||||
{
|
||||
None,
|
||||
Validation,
|
||||
Blocked,
|
||||
StorageError
|
||||
}
|
||||
|
||||
public sealed class KvStateResult
|
||||
{
|
||||
public bool Success => Error == KVChangeError.None;
|
||||
public KVChangeError Error
|
||||
{
|
||||
get;
|
||||
}
|
||||
public long AttemptSequence
|
||||
{
|
||||
get;
|
||||
}
|
||||
public bool LocalCommitMayHaveSucceeded
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal KvStateResult(KVChangeError error, long sequence = 0, bool uncertain = false)
|
||||
{
|
||||
Error = error;
|
||||
AttemptSequence = sequence;
|
||||
LocalCommitMayHaveSucceeded = uncertain;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class KvOpenResult
|
||||
{
|
||||
public bool Success => Error == KVOpenError.None;
|
||||
public KVOpenError Error
|
||||
{
|
||||
get;
|
||||
}
|
||||
public KvSyncState State
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal KvOpenResult(KVOpenError error, KvSyncState state = null)
|
||||
{
|
||||
Error = error;
|
||||
State = state;
|
||||
}
|
||||
}
|
||||
|
||||
// Existence is separate from value: an empty string is not a deletion.
|
||||
public sealed class KvValue : IEquatable<KvValue>
|
||||
{
|
||||
public static readonly KvValue Missing = new KvValue(false, null);
|
||||
public bool Exists
|
||||
{
|
||||
get;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get;
|
||||
}
|
||||
private KvValue(bool exists, string value)
|
||||
{
|
||||
Exists = exists;
|
||||
Value = value;
|
||||
}
|
||||
public static KvValue Present(string value) => new KvValue(true, value ?? throw new ArgumentNullException(nameof(value)));
|
||||
public bool Equals(KvValue other) => other != null && Exists == other.Exists && Value == other.Value;
|
||||
public override bool Equals(object obj) => Equals(obj as KvValue);
|
||||
public override int GetHashCode() => Exists ? (Value.GetHashCode() ^ 397) : 0;
|
||||
internal static KvValue At(IReadOnlyDictionary<string, string> data, string key) =>
|
||||
data.TryGetValue(key, out var value) ? Present(value) : Missing;
|
||||
}
|
||||
|
||||
public sealed class KvChange
|
||||
{
|
||||
public string Key
|
||||
{
|
||||
get;
|
||||
}
|
||||
public long Sequence
|
||||
{
|
||||
get;
|
||||
}
|
||||
public KvValue BaseValue
|
||||
{
|
||||
get;
|
||||
}
|
||||
public KvValue Target
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal KvChange(string key, long sequence, KvValue baseline, KvValue target)
|
||||
{
|
||||
Key = key;
|
||||
Sequence = sequence;
|
||||
BaseValue = baseline;
|
||||
Target = target;
|
||||
}
|
||||
internal KvChange WithBase(KvValue baseline) => new KvChange(Key, Sequence, baseline, Target);
|
||||
}
|
||||
|
||||
public sealed class KvBatch
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get;
|
||||
}
|
||||
public IReadOnlyList<KvChange> Items
|
||||
{
|
||||
get;
|
||||
}
|
||||
public bool IsDelete => !Items[0].Target.Exists;
|
||||
public bool IsOrdinary
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal KvBatch(string id, IEnumerable<KvChange> items, bool ordinary = false)
|
||||
{
|
||||
Id = id;
|
||||
Items = Array.AsReadOnly(items.ToArray());
|
||||
IsOrdinary = ordinary;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class KvDiagnosticSnapshot
|
||||
{
|
||||
public long NextSequence
|
||||
{
|
||||
get;
|
||||
}
|
||||
public KvBatch ActiveBatch
|
||||
{
|
||||
get;
|
||||
}
|
||||
public KvBatch UnknownBatch
|
||||
{
|
||||
get;
|
||||
}
|
||||
public IReadOnlyList<IReadOnlyDictionary<string, KvChange>> OrdinaryBuffers
|
||||
{
|
||||
get;
|
||||
}
|
||||
public IReadOnlyList<KvBatch> PreparedOrdinaryBatches
|
||||
{
|
||||
get;
|
||||
}
|
||||
public IReadOnlyList<KvBatch> OrdinaryUnknownBatches
|
||||
{
|
||||
get;
|
||||
}
|
||||
public IReadOnlyDictionary<string, KvChange> LegacyPending
|
||||
{
|
||||
get;
|
||||
}
|
||||
public IReadOnlyDictionary<string, KvChange> ExplicitDeletes
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal KvDiagnosticSnapshot(KvSnapshot source)
|
||||
{
|
||||
NextSequence = source.NextSequence;
|
||||
ActiveBatch = source.Active;
|
||||
UnknownBatch = source.Unknown;
|
||||
OrdinaryBuffers = Array.AsReadOnly(source.OrdinaryBuffers.Select(bucket => ReadOnly(bucket)).ToArray());
|
||||
PreparedOrdinaryBatches = Array.AsReadOnly(source.PreparedOrdinaryBatches.ToArray());
|
||||
OrdinaryUnknownBatches = Array.AsReadOnly(source.OrdinaryUnknownBatches.ToArray());
|
||||
LegacyPending = ReadOnly(source.LegacyPending);
|
||||
ExplicitDeletes = ReadOnly(source.ExplicitDeletes);
|
||||
}
|
||||
private static IReadOnlyDictionary<string, T> ReadOnly<T>(Dictionary<string, T> source) =>
|
||||
new ReadOnlyDictionary<string, T>(new Dictionary<string, T>(source, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
internal sealed class KvIdentity
|
||||
{
|
||||
internal readonly string Api, PlayerId, DirectoryPath;
|
||||
internal KvIdentity(string root, Uri api, string playerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(root) || !Path.IsPathFullyQualified(root) || api == null || !api.IsAbsoluteUri ||
|
||||
(api.Scheme != "http" && api.Scheme != "https") || api.UserInfo.Length != 0 || api.Query.Length != 0 || api.Fragment.Length != 0 ||
|
||||
!long.TryParse(playerId, NumberStyles.None, CultureInfo.InvariantCulture, out var id) || id <= 0)
|
||||
{
|
||||
throw new ArgumentException("Invalid KV cache identity.");
|
||||
}
|
||||
Api = api.AbsoluteUri.TrimEnd('/') + "/";
|
||||
PlayerId = id.ToString(CultureInfo.InvariantCulture);
|
||||
using (var hash = SHA256.Create())
|
||||
{
|
||||
var bytes = hash.ComputeHash(Encoding.UTF8.GetBytes(Api));
|
||||
var fingerprint = string.Concat(bytes.Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
|
||||
DirectoryPath = Path.Combine(Path.GetFullPath(root), "player-kv", "v1", fingerprint, PlayerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class KvSnapshot
|
||||
{
|
||||
internal long Revision, NextSequence = 1;
|
||||
internal bool HasBaseline, AuthenticationInvalid;
|
||||
internal Dictionary<string, string> Baseline = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
internal Dictionary<string, string> LocalData = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
internal Dictionary<string, long> LocalUpdateTicks = new Dictionary<string, long>(StringComparer.Ordinal);
|
||||
// A local origin has no second copy of its value. Ordinary origins can outlive a sent batch.
|
||||
internal Dictionary<string, (bool LocalOnly, long Sequence)> LocalOrigins =
|
||||
new Dictionary<string, (bool LocalOnly, long Sequence)>(StringComparer.Ordinal);
|
||||
internal HashSet<string> ImmediatePreservedKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
// Only v1/v2 conversion can populate this lower layer; the first successful Get removes it.
|
||||
internal Dictionary<string, string> LegacyLocalOnlyUnderImmediate = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
internal List<Dictionary<string, KvChange>> OrdinaryBuffers = Enumerable.Range(0, 5)
|
||||
.Select(_ => new Dictionary<string, KvChange>(StringComparer.Ordinal)).ToList();
|
||||
internal List<KvBatch> PreparedOrdinaryBatches = new List<KvBatch>();
|
||||
internal List<KvBatch> OrdinaryUnknownBatches = new List<KvBatch>();
|
||||
internal Dictionary<string, KvChange> LegacyPending = new Dictionary<string, KvChange>(StringComparer.Ordinal);
|
||||
internal Dictionary<string, KvChange> ExplicitDeletes = new Dictionary<string, KvChange>(StringComparer.Ordinal);
|
||||
internal Dictionary<string, KvValue> Conflicts = new Dictionary<string, KvValue>(StringComparer.Ordinal);
|
||||
internal Dictionary<long, KvChange> RemoteOnlyWrites = new Dictionary<long, KvChange>();
|
||||
internal KvBatch Active, Unknown;
|
||||
|
||||
internal IEnumerable<KvChange> Unsent()
|
||||
{
|
||||
return OrdinaryBuffers.SelectMany(bucket => bucket.Values).Concat(LegacyPending.Values).Concat(ExplicitDeletes.Values);
|
||||
}
|
||||
internal KvSnapshot Clone()
|
||||
{
|
||||
return new KvSnapshot
|
||||
{
|
||||
Revision = Revision, NextSequence = NextSequence, HasBaseline = HasBaseline, AuthenticationInvalid = AuthenticationInvalid,
|
||||
Baseline = new Dictionary<string, string>(Baseline, StringComparer.Ordinal),
|
||||
LocalData = new Dictionary<string, string>(LocalData, StringComparer.Ordinal),
|
||||
LocalUpdateTicks = new Dictionary<string, long>(LocalUpdateTicks, StringComparer.Ordinal),
|
||||
LocalOrigins = new Dictionary<string, (bool LocalOnly, long Sequence)>(LocalOrigins, StringComparer.Ordinal),
|
||||
ImmediatePreservedKeys = new HashSet<string>(ImmediatePreservedKeys, StringComparer.Ordinal),
|
||||
LegacyLocalOnlyUnderImmediate = new Dictionary<string, string>(LegacyLocalOnlyUnderImmediate, StringComparer.Ordinal),
|
||||
OrdinaryBuffers = OrdinaryBuffers.Select(bucket => new Dictionary<string, KvChange>(bucket, StringComparer.Ordinal)).ToList(),
|
||||
PreparedOrdinaryBatches = new List<KvBatch>(PreparedOrdinaryBatches),
|
||||
OrdinaryUnknownBatches = new List<KvBatch>(OrdinaryUnknownBatches),
|
||||
LegacyPending = new Dictionary<string, KvChange>(LegacyPending, StringComparer.Ordinal),
|
||||
ExplicitDeletes = new Dictionary<string, KvChange>(ExplicitDeletes, StringComparer.Ordinal),
|
||||
Conflicts = new Dictionary<string, KvValue>(Conflicts, StringComparer.Ordinal),
|
||||
Active = Active, Unknown = Unknown,
|
||||
RemoteOnlyWrites = new Dictionary<long, KvChange>(RemoteOnlyWrites)
|
||||
};
|
||||
}
|
||||
internal static bool ValidKey(string key) => !string.IsNullOrEmpty(key) && key.Length <= 128;
|
||||
|
||||
internal string Encode(KvIdentity identity)
|
||||
{
|
||||
var conflicts = new JObject();
|
||||
foreach (var pair in Conflicts)
|
||||
{
|
||||
conflicts.Add(pair.Key, ValueJson(pair.Value));
|
||||
}
|
||||
var origins = new JObject();
|
||||
foreach (var pair in LocalOrigins)
|
||||
{
|
||||
origins.Add(pair.Key, new JObject { ["localOnly"] = pair.Value.LocalOnly, ["sequence"] = pair.Value.Sequence });
|
||||
}
|
||||
return new JObject
|
||||
{
|
||||
["formatVersion"] = 3, ["apiBaseUri"] = identity.Api, ["playerId"] = identity.PlayerId,
|
||||
["revision"] = Revision, ["nextSequence"] = NextSequence, ["hasBaseline"] = HasBaseline,
|
||||
["authenticationInvalid"] = AuthenticationInvalid, ["baseline"] = JObject.FromObject(Baseline),
|
||||
["localData"] = JObject.FromObject(LocalData), ["localUpdateTicks"] = JObject.FromObject(LocalUpdateTicks),
|
||||
["localOrigins"] = origins, ["immediatePreservedKeys"] = new JArray(ImmediatePreservedKeys),
|
||||
["legacyLocalOnlyUnderImmediate"] = JObject.FromObject(LegacyLocalOnlyUnderImmediate),
|
||||
["ordinaryBuffers"] = new JArray(OrdinaryBuffers.Select(bucket => (JToken)ChangesJson(bucket.Values))),
|
||||
["preparedOrdinaryBatches"] = new JArray(PreparedOrdinaryBatches.Select(BatchJson)),
|
||||
["ordinaryUnknownBatches"] = new JArray(OrdinaryUnknownBatches.Select(BatchJson)),
|
||||
["legacyPending"] = ChangesJson(LegacyPending.Values), ["explicitDeletes"] = ChangesJson(ExplicitDeletes.Values),
|
||||
["conflictObservations"] = conflicts, ["activeBatch"] = BatchJson(Active), ["unknownBatch"] = BatchJson(Unknown),
|
||||
["remoteOnlyWrites"] = ChangesJson(RemoteOnlyWrites.Values)
|
||||
}.ToString(Formatting.None);
|
||||
}
|
||||
private static JArray ChangesJson(IEnumerable<KvChange> values)
|
||||
{
|
||||
return new JArray(values.OrderBy(item => item.Sequence).Select(ChangeJson));
|
||||
}
|
||||
private static JObject ValueJson(KvValue value)
|
||||
{
|
||||
return new JObject { ["exists"] = value.Exists, ["value"] = value.Value };
|
||||
}
|
||||
private static JObject ChangeJson(KvChange item)
|
||||
{
|
||||
return new JObject { ["key"] = item.Key, ["sequence"] = item.Sequence,
|
||||
["base"] = ValueJson(item.BaseValue), ["target"] = ValueJson(item.Target) };
|
||||
}
|
||||
private static JToken BatchJson(KvBatch batch)
|
||||
{
|
||||
if (batch == null)
|
||||
{
|
||||
return JValue.CreateNull();
|
||||
}
|
||||
return new JObject { ["id"] = batch.Id, ["ordinary"] = batch.IsOrdinary, ["items"] = ChangesJson(batch.Items) };
|
||||
}
|
||||
|
||||
internal static KvSnapshot Decode(string text, KvIdentity identity)
|
||||
{
|
||||
JObject root;
|
||||
using (var reader = new JsonTextReader(new StringReader(text)) { DateParseHandling = DateParseHandling.None })
|
||||
{
|
||||
root = JObject.Load(reader, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error });
|
||||
if (reader.Read())
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
}
|
||||
var version = Integer(root, "formatVersion");
|
||||
if (version < 1 || version > 3 || String(root, "apiBaseUri") != identity.Api || String(root, "playerId") != identity.PlayerId)
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
var result = new KvSnapshot
|
||||
{
|
||||
Revision = Integer(root, "revision"), NextSequence = Integer(root, "nextSequence"),
|
||||
HasBaseline = Boolean(root, "hasBaseline"), AuthenticationInvalid = Boolean(root, "authenticationInvalid"),
|
||||
Baseline = Strings(Field(root, "baseline", JTokenType.Object)),
|
||||
Active = Batch(root, "activeBatch", version == 3), Unknown = Batch(root, "unknownBatch", version == 3)
|
||||
};
|
||||
foreach (var property in ((JObject)Field(root, "conflictObservations", JTokenType.Object)).Properties())
|
||||
{
|
||||
if (!ValidKey(property.Name))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
result.Conflicts.Add(property.Name, Value(property.Value));
|
||||
}
|
||||
if (version >= 2)
|
||||
{
|
||||
foreach (var item in Field(root, "remoteOnlyWrites", JTokenType.Array))
|
||||
{
|
||||
var change = Change(item);
|
||||
if (!change.Target.Exists || result.RemoteOnlyWrites.ContainsKey(change.Sequence))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
result.RemoteOnlyWrites.Add(change.Sequence, change);
|
||||
}
|
||||
}
|
||||
if (version < 3)
|
||||
{
|
||||
DecodeLegacy(root, version, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.LocalData = Strings(Field(root, "localData", JTokenType.Object));
|
||||
result.LegacyLocalOnlyUnderImmediate = Strings(Field(root, "legacyLocalOnlyUnderImmediate", JTokenType.Object));
|
||||
foreach (var property in ((JObject)Field(root, "localUpdateTicks", JTokenType.Object)).Properties())
|
||||
{
|
||||
if (!ValidKey(property.Name) || property.Value.Type != JTokenType.Integer)
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
result.LocalUpdateTicks.Add(property.Name, (long)property.Value);
|
||||
}
|
||||
foreach (var property in ((JObject)Field(root, "localOrigins", JTokenType.Object)).Properties())
|
||||
{
|
||||
if (!ValidKey(property.Name))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
result.LocalOrigins.Add(property.Name, (Boolean(property.Value, "localOnly"), Integer(property.Value, "sequence")));
|
||||
}
|
||||
foreach (var key in Field(root, "immediatePreservedKeys", JTokenType.Array))
|
||||
{
|
||||
if (key.Type != JTokenType.String || !ValidKey((string)key) || !result.ImmediatePreservedKeys.Add((string)key))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
}
|
||||
result.OrdinaryBuffers = Field(root, "ordinaryBuffers", JTokenType.Array).Select(Changes).ToList();
|
||||
result.LegacyPending = Changes(Field(root, "legacyPending", JTokenType.Array));
|
||||
result.ExplicitDeletes = Changes(Field(root, "explicitDeletes", JTokenType.Array));
|
||||
result.PreparedOrdinaryBatches = ReadBatches(root, "preparedOrdinaryBatches");
|
||||
result.OrdinaryUnknownBatches = ReadBatches(root, "ordinaryUnknownBatches");
|
||||
}
|
||||
result.Validate();
|
||||
return result;
|
||||
}
|
||||
private static Dictionary<string, KvChange> Changes(JToken array)
|
||||
{
|
||||
if (array.Type != JTokenType.Array)
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
var values = new Dictionary<string, KvChange>(StringComparer.Ordinal);
|
||||
foreach (var token in array)
|
||||
{
|
||||
var item = Change(token);
|
||||
if (values.ContainsKey(item.Key))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
values.Add(item.Key, item);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
private static List<KvBatch> ReadBatches(JToken root, string name)
|
||||
{
|
||||
var result = new List<KvBatch>();
|
||||
foreach (var item in Field(root, name, JTokenType.Array))
|
||||
{
|
||||
var wrapper = new JObject { ["batch"] = item.DeepClone() };
|
||||
var batch = Batch(wrapper, "batch", true);
|
||||
if (batch == null || !batch.IsOrdinary || batch.IsDelete)
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
result.Add(batch);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void DecodeLegacy(JObject root, long version, KvSnapshot result)
|
||||
{
|
||||
var pending = Changes(Field(root, "pending", JTokenType.Array));
|
||||
var localOnly = Strings(Field(root, "localOnly", JTokenType.Object));
|
||||
var overrides = new Dictionary<string, KvValue>(StringComparer.Ordinal);
|
||||
if (version == 2)
|
||||
{
|
||||
foreach (var property in ((JObject)Field(root, "remoteLocalOverrides", JTokenType.Object)).Properties())
|
||||
{
|
||||
if (!ValidKey(property.Name))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
overrides.Add(property.Name, Value(property.Value));
|
||||
}
|
||||
}
|
||||
// Validate the old shape before converting; new format rules must not accept a corrupt old file.
|
||||
if (result.Revision < 1 || result.NextSequence < 1 || (result.Active != null && result.Unknown != null) ||
|
||||
(!result.HasBaseline && (result.Baseline.Count != 0 || localOnly.Count != 0 || pending.Count != 0 ||
|
||||
result.Active != null || result.Unknown != null || overrides.Count != 0 || result.RemoteOnlyWrites.Count != 0)))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
var sequences = new HashSet<long>();
|
||||
foreach (var item in pending.Values.Concat(result.RemoteOnlyWrites.Values))
|
||||
{
|
||||
if (item.Sequence < 1 || item.Sequence >= result.NextSequence || !sequences.Add(item.Sequence))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
}
|
||||
if (result.Conflicts.Keys.Any(key => !pending.ContainsKey(key)))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
var oldBatch = result.Active ?? result.Unknown;
|
||||
if (oldBatch != null)
|
||||
{
|
||||
foreach (var item in oldBatch.Items)
|
||||
{
|
||||
var hasPending = pending.TryGetValue(item.Key, out var latest);
|
||||
var hasSource = result.RemoteOnlyWrites.Values.Any(source => source.Key == item.Key && source.Sequence > item.Sequence);
|
||||
var same = hasPending && latest.Sequence == item.Sequence && latest.BaseValue.Equals(item.BaseValue) && latest.Target.Equals(item.Target);
|
||||
if (item.Sequence < 1 || item.Sequence >= result.NextSequence || (sequences.Contains(item.Sequence) && !same) ||
|
||||
(!hasPending && !hasSource) || (hasPending && (latest.Sequence < item.Sequence || !latest.BaseValue.Equals(item.BaseValue) ||
|
||||
(latest.Sequence == item.Sequence && !same))))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
sequences.Add(item.Sequence);
|
||||
}
|
||||
}
|
||||
result.LegacyPending = pending;
|
||||
result.LocalData = new Dictionary<string, string>(result.Baseline, StringComparer.Ordinal);
|
||||
foreach (var item in pending.Values)
|
||||
{
|
||||
ApplyValue(result.LocalData, item.Key, item.Target);
|
||||
result.LocalOrigins[item.Key] = (false, item.Sequence);
|
||||
}
|
||||
foreach (var pair in localOnly)
|
||||
{
|
||||
result.LocalData[pair.Key] = pair.Value;
|
||||
// Old local-only writes had no stored sequence; zero is permitted only for this converted origin.
|
||||
result.LocalOrigins[pair.Key] = (true, 0);
|
||||
}
|
||||
foreach (var pair in overrides)
|
||||
{
|
||||
ApplyValue(result.LocalData, pair.Key, pair.Value);
|
||||
result.ImmediatePreservedKeys.Add(pair.Key);
|
||||
if (localOnly.TryGetValue(pair.Key, out var lower))
|
||||
{
|
||||
result.LegacyLocalOnlyUnderImmediate[pair.Key] = lower;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void ApplyValue(Dictionary<string, string> values, string key, KvValue value)
|
||||
{
|
||||
if (value.Exists)
|
||||
{
|
||||
values[key] = value.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
values.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
internal void Validate()
|
||||
{
|
||||
if (Revision < 1 || NextSequence < 1 || OrdinaryBuffers.Count != 5 ||
|
||||
(Active != null && Unknown != null) || (Unknown != null && Unknown.IsOrdinary) ||
|
||||
OrdinaryBuffers.Any(bucket => bucket.Count > 10 || bucket.Values.Any(item => !item.Target.Exists)) ||
|
||||
ExplicitDeletes.Values.Any(item => item.Target.Exists) ||
|
||||
(!HasBaseline && (Baseline.Count != 0 || LocalData.Count != 0 || Unsent().Any() || Active != null || Unknown != null ||
|
||||
PreparedOrdinaryBatches.Count != 0 || OrdinaryUnknownBatches.Count != 0 || RemoteOnlyWrites.Count != 0 ||
|
||||
LocalOrigins.Count != 0 || LocalUpdateTicks.Count != 0 || ImmediatePreservedKeys.Count != 0 || LegacyLocalOnlyUnderImmediate.Count != 0)))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
var sequences = new Dictionary<long, KvChange>();
|
||||
void Add(KvChange item, bool sourceReference = false)
|
||||
{
|
||||
if (!ValidKey(item.Key) || item.Sequence < 1 || item.Sequence >= NextSequence)
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
if (sequences.TryGetValue(item.Sequence, out var prior))
|
||||
{
|
||||
if (!sourceReference || prior.Key != item.Key || !prior.BaseValue.Equals(item.BaseValue) || !prior.Target.Equals(item.Target))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sequences.Add(item.Sequence, item);
|
||||
}
|
||||
}
|
||||
foreach (var item in Unsent().Concat(RemoteOnlyWrites.Values))
|
||||
{
|
||||
Add(item);
|
||||
}
|
||||
var ids = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var batch in PreparedOrdinaryBatches.Concat(OrdinaryUnknownBatches).Concat(new[] { Active, Unknown }).Where(batch => batch != null))
|
||||
{
|
||||
if (!Guid.TryParseExact(batch.Id, "N", out _) || !ids.Add(batch.Id) || batch.Items.Count < 1 || batch.Items.Count > 10 ||
|
||||
batch.Items.Select(item => item.Key).Distinct(StringComparer.Ordinal).Count() != batch.Items.Count ||
|
||||
batch.Items.Any(item => item.Target.Exists != batch.Items[0].Target.Exists) || (batch.IsOrdinary && batch.IsDelete) ||
|
||||
!batch.Items.Select(item => item.Sequence).SequenceEqual(batch.Items.Select(item => item.Sequence).OrderBy(value => value)))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
foreach (var item in batch.Items)
|
||||
{
|
||||
var reference = !batch.IsOrdinary && (LegacyPending.Values.Concat(ExplicitDeletes.Values)
|
||||
.Any(source => source.Sequence == item.Sequence));
|
||||
Add(item, reference);
|
||||
}
|
||||
}
|
||||
if (RemoteOnlyWrites.Any(pair => pair.Key != pair.Value.Sequence || !pair.Value.Target.Exists) ||
|
||||
PreparedOrdinaryBatches.Concat(OrdinaryUnknownBatches).Any(batch => !batch.IsOrdinary) ||
|
||||
LocalOrigins.Any(pair => !ValidKey(pair.Key) || pair.Value.Sequence < 0 || pair.Value.Sequence >= NextSequence ||
|
||||
(!pair.Value.LocalOnly && pair.Value.Sequence == 0)) ||
|
||||
LocalUpdateTicks.Any(pair => !ValidKey(pair.Key) || pair.Value < 0 || pair.Value > DateTime.MaxValue.Ticks) ||
|
||||
ImmediatePreservedKeys.Any(key => !ValidKey(key)) ||
|
||||
LegacyLocalOnlyUnderImmediate.Any(pair => !ImmediatePreservedKeys.Contains(pair.Key) ||
|
||||
!LocalOrigins.TryGetValue(pair.Key, out var origin) || !origin.LocalOnly) ||
|
||||
Conflicts.Keys.Any(key => !Unsent().Any(item => item.Key == key) && !PreparedOrdinaryBatches.Any(batch => batch.Items.Any(item => item.Key == key)) &&
|
||||
Unknown == null && OrdinaryUnknownBatches.Count == 0))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
}
|
||||
private static Dictionary<string, string> Strings(JToken value)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var property in ((JObject)value).Properties())
|
||||
{
|
||||
if (!ValidKey(property.Name) || property.Value.Type != JTokenType.String)
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
result.Add(property.Name, (string)property.Value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static KvChange Change(JToken item)
|
||||
{
|
||||
var key = String(item, "key");
|
||||
if (!ValidKey(key))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
return new KvChange(key, Integer(item, "sequence"), Value(Field(item, "base", JTokenType.Object)), Value(Field(item, "target", JTokenType.Object)));
|
||||
}
|
||||
private static KvValue Value(JToken item)
|
||||
{
|
||||
var exists = Boolean(item, "exists");
|
||||
var value = Field(item, "value", exists ? JTokenType.String : JTokenType.Null);
|
||||
return exists ? KvValue.Present((string)value) : KvValue.Missing;
|
||||
}
|
||||
private static KvBatch Batch(JToken parent, string name, bool withKind = false)
|
||||
{
|
||||
var node = parent[name];
|
||||
if (node == null)
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
if (node.Type == JTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var id = String(node, "id");
|
||||
if (!Guid.TryParseExact(id, "N", out _))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
var items = Field(node, "items", JTokenType.Array).Select(Change).ToArray();
|
||||
if (items.Length == 0 || items.Length > 10 || items.Select(i => i.Key).Distinct(StringComparer.Ordinal).Count() != items.Length ||
|
||||
items.Select(i => i.Sequence).Distinct().Count() != items.Length || items.Any(i => i.Target.Exists != items[0].Target.Exists) ||
|
||||
!items.Select(i => i.Sequence).SequenceEqual(items.Select(i => i.Sequence).OrderBy(s => s)))
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
return new KvBatch(id, items, withKind && Boolean(node, "ordinary"));
|
||||
}
|
||||
private static JToken Field(JToken parent, string name, JTokenType type)
|
||||
{
|
||||
if (!(parent is JObject obj) || obj[name] == null || obj[name].Type != type)
|
||||
{
|
||||
throw Invalid();
|
||||
}
|
||||
return obj[name];
|
||||
}
|
||||
private static string String(JToken parent, string name) => (string)Field(parent, name, JTokenType.String);
|
||||
private static long Integer(JToken parent, string name) => (long)Field(parent, name, JTokenType.Integer);
|
||||
private static bool Boolean(JToken parent, string name) => (bool)Field(parent, name, JTokenType.Boolean);
|
||||
internal static JsonException Invalid() => new JsonException("Invalid KV cache snapshot.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user