using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace game.PlayerDataStorage
{
///
/// Synchronous, fixed-identity cache state. No HTTP, gameplay initialization or timers.
/// All transitions and disk writes share a gate. The phase-3B owner must drain its
/// admitted network operations before disposing this state and its filesystem lease.
///
public sealed class PlayerKVSyncState : IDisposable
{
private readonly object gate = new object();
private readonly PlayerKVCacheStore store;
private KVSnapshot snapshot;
private PlayerKVSyncStatus status;
private readonly HashSet recoveredImmediate;
private bool recoverImmediateView = true;
private bool recoveredOrdinaryUnknown;
private KVSnapshot comparisonLocal;
private Dictionary comparisonRemote;
private Dictionary comparisonTimes;
private Dictionary comparisonView;
private HashSet comparisonImmediateKeys;
internal bool HasPendingComparison
{
get
{
lock (gate)
{
return comparisonLocal != null;
}
}
}
internal bool AuthenticationInvalid
{
get
{
lock (gate)
{
return snapshot.AuthenticationInvalid;
}
}
}
public PlayerKVSyncStatus Status { get { lock (gate) return status; } }
public string ApiBaseUri { get; }
public string PlayerId { get; }
PlayerKVSyncState(KVIdentity identity, PlayerKVCacheStore store, KVSnapshot snapshot)
{
ApiBaseUri = identity.Api; PlayerId = identity.PlayerId; this.store = store; this.snapshot = snapshot;
recoveredImmediate = new HashSet(snapshot.RemoteOnlyWrites.Keys);
// Authentication belongs to the old owner. A new owner must obtain a fresh
// successful Get before Ready; this never clears a persisted unknown batch.
snapshot.AuthenticationInvalid = false;
// A recovered active batch might have reached the server. Never acknowledge it by guessing.
if (snapshot.Active != null)
{
if (snapshot.Active.IsOrdinary)
{
snapshot.OrdinaryUnknownBatches.Add(snapshot.Active);
}
else
{
snapshot.Unknown = snapshot.Active;
}
snapshot.Active = null;
}
recoveredOrdinaryUnknown = snapshot.OrdinaryUnknownBatches.Count != 0;
status = snapshot.Unknown != null || recoveredOrdinaryUnknown
? PlayerKVSyncStatus.UnknownOutcome : PlayerKVSyncStatus.NotLoaded;
}
public static PlayerKVOpenResult Open(string root, Uri api, string playerId) => Open(root, api, playerId, null);
internal static PlayerKVOpenResult Open(string root, Uri api, string playerId, Action fault)
{
KVIdentity identity;
try { identity = new KVIdentity(root, api, playerId); }
catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException)
{ return new PlayerKVOpenResult(PlayerKVOpenError.Validation); }
var error = PlayerKVCacheStore.Open(identity, fault, out var store, out var snapshot);
return error == PlayerKVOpenError.None ? new PlayerKVOpenResult(error, new PlayerKVSyncState(identity, store, snapshot)) : new PlayerKVOpenResult(error);
}
public PlayerKVDiagnosticSnapshot Inspect() { lock (gate) return new PlayerKVDiagnosticSnapshot(snapshot); }
public IReadOnlyDictionary ReadLocal()
{
lock (gate)
{
if (status != PlayerKVSyncStatus.Ready) throw new InvalidOperationException("KV data is not ready.");
return new ReadOnlyDictionary(ReadView());
}
}
// The caller must supply a successful complete Get. Failed reads must use RecordReadFailure.
public PlayerKVStateResult Reconcile(IReadOnlyDictionary remote)
{
return Reconcile(remote, null, false);
}
internal PlayerKVStateResult Reconcile(IReadOnlyDictionary remote,
IReadOnlyDictionary updatedAtUtc, bool prepareOriginalComparison)
{
lock (gate)
{
if (Terminal || snapshot.Active != null || snapshot.AuthenticationInvalid || comparisonLocal != null)
{
return Blocked();
}
if (remote == null || remote.Any(pair => !KVSnapshot.ValidKey(pair.Key) || pair.Value == null))
{
return Invalid();
}
if (prepareOriginalComparison && (!recoverImmediateView || updatedAtUtc == null ||
updatedAtUtc.Count != remote.Count || remote.Keys.Any(key => !updatedAtUtc.ContainsKey(key)) ||
updatedAtUtc.Values.Any(time => time.Kind != DateTimeKind.Utc)))
{
return Invalid();
}
var original = snapshot;
var copy = snapshot.Clone();
copy.Conflicts.Clear();
if (copy.Unknown != null || (recoveredOrdinaryUnknown && !prepareOriginalComparison))
{
foreach (var item in copy.Unsent())
{
copy.Conflicts[item.Key] = PlayerKVValue.At(remote, item.Key);
}
return Commit(copy, PlayerKVSyncStatus.UnknownOutcome);
}
var recoveringKeys = recoverImmediateView
? new HashSet(copy.ImmediatePreservedKeys, StringComparer.Ordinal)
: new HashSet(StringComparer.Ordinal);
if (recoverImmediateView)
{
foreach (var sequence in recoveredImmediate)
{
var source = copy.RemoteOnlyWrites[sequence];
Rebase(copy, source.Key, PlayerKVValue.At(remote, source.Key));
if (!prepareOriginalComparison)
{
copy.RemoteOnlyWrites.Remove(sequence);
}
}
if (!prepareOriginalComparison)
{
copy.ImmediatePreservedKeys.Clear();
}
}
foreach (var source in Sources(copy))
{
if (prepareOriginalComparison && copy.OrdinaryBuffers.Contains(source))
{
continue;
}
foreach (var item in source.Values.ToArray())
{
var observed = PlayerKVValue.At(remote, item.Key);
if (observed.Equals(item.Target))
{
source.Remove(item.Key);
}
else if (!observed.Equals(item.BaseValue))
{
copy.Conflicts[item.Key] = observed;
}
}
}
foreach (var item in copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
{
var observed = PlayerKVValue.At(remote, item.Key);
if (!observed.Equals(item.Target) && !observed.Equals(item.BaseValue))
{
copy.Conflicts[item.Key] = observed;
}
}
var visible = new Dictionary(remote, StringComparer.Ordinal);
foreach (var pair in copy.LocalOrigins.ToArray())
{
if (pair.Value.LocalOnly)
{
if (recoverImmediateView && copy.LegacyLocalOnlyUnderImmediate.TryGetValue(pair.Key, out var lower))
{
visible[pair.Key] = lower;
}
else
{
Apply(visible, pair.Key, PlayerKVValue.At(copy.LocalData, pair.Key));
}
}
else
{
var source = copy.Unsent().Concat(copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
.Concat(copy.OrdinaryUnknownBatches.SelectMany(batch => batch.Items))
.FirstOrDefault(item => item.Sequence == pair.Value.Sequence);
if (!recoverImmediateView)
{
Apply(visible, pair.Key, PlayerKVValue.At(copy.LocalData, pair.Key));
}
else if (source != null)
{
Apply(visible, pair.Key, recoveringKeys.Contains(pair.Key)
? source.Target : PlayerKVValue.At(copy.LocalData, pair.Key));
}
else
{
copy.LocalOrigins.Remove(pair.Key);
}
}
}
// An immediate write preserves its view until a new owner performs its initial Get.
foreach (var key in copy.ImmediatePreservedKeys)
{
if (prepareOriginalComparison && recoveringKeys.Contains(key))
{
continue;
}
Apply(visible, key, PlayerKVValue.At(copy.LocalData, key));
}
if (recoverImmediateView && !prepareOriginalComparison)
{
copy.LegacyLocalOnlyUnderImmediate.Clear();
}
if (!prepareOriginalComparison)
{
copy.LocalData = visible;
}
copy.Baseline = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
copy.HasBaseline = true;
var reconciled = Commit(copy, copy.Conflicts.Count == 0 ? PlayerKVSyncStatus.Ready : PlayerKVSyncStatus.Conflict);
if (reconciled.Success)
{
recoverImmediateView = false;
if (prepareOriginalComparison)
{
comparisonLocal = original;
comparisonRemote = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
comparisonTimes = updatedAtUtc.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
comparisonView = visible;
comparisonImmediateKeys = new HashSet(StringComparer.Ordinal);
}
else
{
recoveredImmediate.Clear();
}
}
return reconciled;
}
}
private Dictionary ReadView()
{
if (comparisonLocal == null)
{
return LocalView(snapshot);
}
var view = new Dictionary(comparisonView, StringComparer.Ordinal);
foreach (var pair in snapshot.LocalOrigins)
{
if (pair.Value.Sequence >= comparisonLocal.NextSequence)
{
Apply(view, pair.Key, PlayerKVValue.At(snapshot.LocalData, pair.Key));
}
}
return view;
}
internal PlayerKVStateResult CompareDatas(long nowTicks)
{
lock (gate)
{
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
snapshot.Unknown != null || comparisonLocal == null)
{
return Blocked();
}
if ((comparisonLocal.OrdinaryUnknownBatches.Count != 0 && !recoveredOrdinaryUnknown) ||
snapshot.OrdinaryUnknownBatches.Any(batch => !comparisonLocal.OrdinaryUnknownBatches.Any(initial => initial.Id == batch.Id)))
{
return Blocked();
}
if (nowTicks < 0 || nowTicks > DateTime.MaxValue.Ticks)
{
return Invalid();
}
var copy = snapshot.Clone();
// A new login uses the original time comparison, not the old request's outcome.
// Retire only its initial ordinary evidence in the same commit as that comparison.
foreach (var batch in comparisonLocal.OrdinaryUnknownBatches)
{
copy.OrdinaryUnknownBatches.RemoveAll(current => current.Id == batch.Id);
foreach (var item in batch.Items)
{
if (copy.LocalOrigins.TryGetValue(item.Key, out var origin) && origin.Sequence == item.Sequence)
{
copy.LocalOrigins.Remove(item.Key);
}
}
}
var protectedKeys = new HashSet(StringComparer.Ordinal);
foreach (var source in new[] { comparisonLocal, snapshot })
{
protectedKeys.UnionWith(source.PreparedOrdinaryBatches.SelectMany(batch => batch.Items).Select(item => item.Key));
protectedKeys.UnionWith(source.LegacyPending.Keys);
protectedKeys.UnionWith(source.ExplicitDeletes.Keys);
protectedKeys.UnionWith(source.ImmediatePreservedKeys);
protectedKeys.UnionWith(source.RemoteOnlyWrites.Values.Select(item => item.Key));
}
protectedKeys.UnionWith(snapshot.LocalOrigins.Where(pair => pair.Value.Sequence >= comparisonLocal.NextSequence).Select(pair => pair.Key));
var visible = ReadView();
foreach (var key in protectedKeys)
{
Apply(copy.LocalData, key, PlayerKVValue.At(visible, key));
}
foreach (var key in comparisonLocal.LocalData.Keys.Concat(comparisonRemote.Keys).Distinct(StringComparer.Ordinal))
{
if (protectedKeys.Contains(key))
{
continue;
}
var localExists = comparisonLocal.HasBaseline && comparisonLocal.LocalData.TryGetValue(key, out _);
comparisonLocal.LocalData.TryGetValue(key, out var localValue);
var remoteExists = comparisonRemote.TryGetValue(key, out var remoteValue);
var localTime = comparisonLocal.LocalUpdateTicks.TryGetValue(key, out var ticks) ? ticks : 0;
var remoteTime = remoteExists ? comparisonTimes[key].Ticks : 0;
if (!localExists || (remoteExists && remoteTime - localTime > TimeSpan.TicksPerSecond * 60))
{
copy.LocalData[key] = remoteValue;
copy.LocalUpdateTicks[key] = remoteTime;
foreach (var bucket in copy.OrdinaryBuffers)
{
if (bucket.TryGetValue(key, out var current) && comparisonLocal.OrdinaryBuffers.Any(old =>
old.TryGetValue(key, out var item) && item.Sequence == current.Sequence))
{
bucket.Remove(key);
if (copy.LocalOrigins.TryGetValue(key, out var origin) && origin.Sequence == current.Sequence)
{
copy.LocalOrigins.Remove(key);
}
}
}
}
else if (!remoteExists || localTime - remoteTime > TimeSpan.TicksPerSecond * 60)
{
if (copy.NextSequence == long.MaxValue)
{
return Invalid();
}
ApplyUpdate(copy, key, localValue, true, nowTicks);
}
else
{
copy.LocalData[key] = localValue;
copy.LocalUpdateTicks[key] = localTime;
}
}
foreach (var sequence in comparisonLocal.RemoteOnlyWrites.Keys)
{
copy.RemoteOnlyWrites.Remove(sequence);
}
foreach (var key in comparisonLocal.LegacyLocalOnlyUnderImmediate.Keys)
{
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
}
foreach (var key in comparisonLocal.ImmediatePreservedKeys)
{
if (!comparisonImmediateKeys.Contains(key))
{
copy.ImmediatePreservedKeys.Remove(key);
}
}
var result = Commit(copy, PlayerKVSyncStatus.Ready);
if (result.Success)
{
recoveredOrdinaryUnknown = false;
ClearComparison();
recoveredImmediate.Clear();
}
return result;
}
}
private void ClearComparison()
{
comparisonLocal = null;
comparisonRemote = null;
comparisonTimes = null;
comparisonView = null;
comparisonImmediateKeys = null;
}
public PlayerKVStateResult RecordReadFailure(bool unauthorized = false)
{
lock (gate)
{
if (Terminal || snapshot.Active != null) return Blocked();
if (unauthorized)
{
var copy = snapshot.Clone(); copy.AuthenticationInvalid = true;
return Commit(copy, copy.Unknown == null ? PlayerKVSyncStatus.Unauthorized : PlayerKVSyncStatus.UnknownOutcome);
}
status = snapshot.Unknown != null ? PlayerKVSyncStatus.UnknownOutcome : PlayerKVSyncStatus.ReadFailed;
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
}
}
public PlayerKVStateResult Update(string key, string value, bool remoteStore = true)
{
return Update(key, value, remoteStore, null);
}
internal PlayerKVStateResult Update(string key, string value, bool remoteStore, long? updateTicks)
{
lock (gate)
{
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (!KVSnapshot.ValidKey(key) || value == null || snapshot.NextSequence == long.MaxValue ||
(updateTicks.HasValue && (updateTicks.Value < 0 || updateTicks.Value > DateTime.MaxValue.Ticks)))
{
return Invalid();
}
var copy = snapshot.Clone();
var sequence = ApplyUpdate(copy, key, value, remoteStore, updateTicks);
return Commit(copy, PlayerKVSyncStatus.Ready, sequence);
}
}
private static long ApplyUpdate(KVSnapshot copy, string key, string value, bool remoteStore, long? updateTicks)
{
var sequence = copy.NextSequence++;
copy.LocalData[key] = value;
copy.ImmediatePreservedKeys.Remove(key);
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
if (updateTicks.HasValue)
{
copy.LocalUpdateTicks[key] = updateTicks.Value;
}
else
{
copy.LocalUpdateTicks.Remove(key);
}
var acceptedRemote = false;
if (remoteStore)
{
foreach (var bucket in copy.OrdinaryBuffers)
{
if (!bucket.ContainsKey(key) && bucket.Count >= 10)
{
continue;
}
var baseline = bucket.TryGetValue(key, out var previous)
? previous.BaseValue : PlayerKVValue.At(copy.Baseline, key);
bucket[key] = new PlayerKVChange(key, sequence, baseline, PlayerKVValue.Present(value));
copy.LegacyPending.Remove(key);
copy.ExplicitDeletes.Remove(key);
acceptedRemote = true;
break;
}
}
copy.LocalOrigins[key] = (!acceptedRemote, sequence);
return sequence;
}
public PlayerKVStateResult Delete(string key)
{
lock (gate)
{
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (!KVSnapshot.ValidKey(key) || snapshot.NextSequence == long.MaxValue)
{
return Invalid();
}
var copy = snapshot.Clone();
var sequence = copy.NextSequence++;
var prior = copy.ExplicitDeletes.TryGetValue(key, out var previous) ? previous.BaseValue : PlayerKVValue.At(copy.Baseline, key);
RemoveSources(copy, key);
copy.ExplicitDeletes[key] = new PlayerKVChange(key, sequence, prior, PlayerKVValue.Missing);
copy.LocalData.Remove(key);
copy.LocalUpdateTicks.Remove(key);
copy.LocalOrigins[key] = (false, sequence);
copy.ImmediatePreservedKeys.Remove(key);
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
return Commit(copy, PlayerKVSyncStatus.Ready, sequence);
}
}
internal PlayerKVStateResult CaptureOrdinaryBatch(out PlayerKVBatch batch)
{
lock (gate)
{
batch = null;
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (snapshot.OrdinaryBuffers[0].Count == 0)
{
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
}
var copy = snapshot.Clone();
var first = copy.OrdinaryBuffers[0];
var captured = new PlayerKVBatch(Guid.NewGuid().ToString("N"), first.Values.OrderBy(item => item.Sequence), true);
copy.PreparedOrdinaryBatches.Add(captured);
copy.OrdinaryBuffers.RemoveAt(0);
copy.OrdinaryBuffers.Add(new Dictionary(StringComparer.Ordinal));
var result = Commit(copy, PlayerKVSyncStatus.Ready);
if (result.Success)
{
batch = captured;
}
return result;
}
}
internal PlayerKVStateResult BeginOrdinaryBatch(out PlayerKVBatch batch)
{
lock (gate)
{
batch = null;
if (status != PlayerKVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (snapshot.PreparedOrdinaryBatches.Count == 0)
{
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
}
var copy = snapshot.Clone();
copy.Active = copy.PreparedOrdinaryBatches[0];
copy.PreparedOrdinaryBatches.RemoveAt(0);
var result = Commit(copy, PlayerKVSyncStatus.Ready);
if (result.Success)
{
batch = snapshot.Active;
}
return result;
}
}
// Explicit requests retain their original source until the outcome is known. Ordinary buckets are never selected here.
public PlayerKVStateResult BeginBatch(long throughSequence, out PlayerKVBatch batch)
{
return BeginBatch(throughSequence, false, out batch);
}
internal PlayerKVStateResult BeginBatch(long throughSequence, bool legacyOnly, out PlayerKVBatch batch)
{
lock (gate)
{
batch = null;
if (status != PlayerKVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (throughSequence < 0)
{
return Invalid();
}
var source = legacyOnly ? snapshot.LegacyPending.Values : snapshot.LegacyPending.Values.Concat(snapshot.ExplicitDeletes.Values);
var ordered = source.Where(item => item.Sequence <= throughSequence).OrderBy(item => item.Sequence).ToArray();
if (ordered.Length == 0)
{
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
}
var copy = snapshot.Clone();
copy.Active = new PlayerKVBatch(Guid.NewGuid().ToString("N"), ordered.TakeWhile(item => item.Target.Exists == ordered[0].Target.Exists).Take(10));
var result = Commit(copy, PlayerKVSyncStatus.Ready);
if (result.Success)
{
batch = snapshot.Active;
}
return result;
}
}
public PlayerKVStateResult ConfirmBatch(string batchId)
{
lock (gate)
{
if (status != PlayerKVSyncStatus.Ready || snapshot.Active == null || snapshot.Active.Id != batchId)
{
return Blocked();
}
var copy = snapshot.Clone();
foreach (var item in copy.Active.Items)
{
Apply(copy.Baseline, item.Key, item.Target);
if (!copy.Active.IsOrdinary)
{
foreach (var source in new[] { copy.LegacyPending, copy.ExplicitDeletes })
{
if (source.TryGetValue(item.Key, out var current) && current.Sequence == item.Sequence)
{
source.Remove(item.Key);
}
}
}
Rebase(copy, item.Key, item.Target);
}
copy.Active = null;
return Commit(copy, PlayerKVSyncStatus.Ready);
}
}
// A definitely undispatched ordinary batch retires, as the original SDK call did; explicit source references stay.
public PlayerKVStateResult FailBatch(string batchId, bool mayHaveCommitted, bool unauthorized = false)
{
lock (gate)
{
if (Terminal || snapshot.Active == null || snapshot.Active.Id != batchId)
{
return Blocked();
}
var copy = snapshot.Clone();
var ordinary = copy.Active.IsOrdinary;
copy.AuthenticationInvalid |= unauthorized;
if (mayHaveCommitted)
{
if (ordinary)
{
copy.OrdinaryUnknownBatches.Add(copy.Active);
}
else
{
copy.Unknown = copy.Active;
}
}
copy.Active = null;
var next = copy.AuthenticationInvalid ? PlayerKVSyncStatus.Unauthorized : ordinary ? PlayerKVSyncStatus.Ready : PlayerKVSyncStatus.ReadFailed;
if (copy.Unknown != null)
{
next = PlayerKVSyncStatus.UnknownOutcome;
}
return Commit(copy, next);
}
}
private static IEnumerable> Sources(KVSnapshot value)
{
return value.OrdinaryBuffers.Concat(new[] { value.LegacyPending, value.ExplicitDeletes });
}
private static void RemoveSources(KVSnapshot value, string key)
{
foreach (var source in Sources(value))
{
source.Remove(key);
}
}
private static void Rebase(KVSnapshot value, string key, PlayerKVValue baseline)
{
foreach (var source in Sources(value))
{
if (source.TryGetValue(key, out var pending))
{
source[key] = pending.WithBase(baseline);
}
}
}
internal PlayerKVStateResult PrepareImmediate(string key, string value, out PlayerKVChange operation)
{
operation = null;
if (!KVSnapshot.ValidKey(key) || value == null)
{
// Preserve the single-key state admission order and validation result.
lock (gate)
{
return status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid ? Blocked() : Invalid();
}
}
var result = PrepareImmediate(new Dictionary { [key] = value }, out var operations);
if (result.Success)
{
operation = operations[0];
}
return result;
}
internal PlayerKVStateResult PrepareImmediate(IReadOnlyDictionary data,
out IReadOnlyList operations)
{
lock (gate)
{
operations = null;
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (data == null || data.Count == 0 || data.Count > long.MaxValue - snapshot.NextSequence ||
data.Any(pair => !KVSnapshot.ValidKey(pair.Key) || pair.Value == null))
{
return Invalid();
}
var copy = snapshot.Clone();
var prepared = new List();
foreach (var pair in data)
{
copy.ImmediatePreservedKeys.Add(pair.Key);
var operation = new PlayerKVChange(pair.Key, copy.NextSequence++,
PlayerKVValue.At(copy.Baseline, pair.Key), PlayerKVValue.Present(pair.Value));
copy.RemoteOnlyWrites.Add(operation.Sequence, operation);
RemoveSources(copy, pair.Key);
prepared.Add(operation);
}
var result = Commit(copy, PlayerKVSyncStatus.Ready, prepared[0].Sequence);
if (result.Success)
{
operations = prepared.AsReadOnly();
if (comparisonLocal != null)
{
comparisonImmediateKeys.UnionWith(data.Keys);
}
}
return result;
}
}
internal PlayerKVStateResult CompleteImmediate(PlayerKVChange operation)
{
return CompleteImmediate(new[] { operation });
}
internal PlayerKVStateResult CompleteImmediate(IReadOnlyList operations)
{
lock (gate)
{
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
snapshot.Unknown != null || operations.Any(operation => !snapshot.RemoteOnlyWrites.ContainsKey(operation.Sequence)))
{
return Blocked();
}
var copy = snapshot.Clone();
foreach (var operation in operations)
{
Apply(copy.Baseline, operation.Key, operation.Target);
Rebase(copy, operation.Key, operation.Target);
copy.RemoteOnlyWrites.Remove(operation.Sequence);
copy.ImmediatePreservedKeys.Add(operation.Key);
}
return Commit(copy, PlayerKVSyncStatus.Ready);
}
}
internal void CancelUndispatchedImmediate(PlayerKVChange operation)
{
CancelUndispatchedImmediate(new[] { operation });
}
internal void CancelUndispatchedImmediate(IReadOnlyList operations)
{
lock (gate)
{
if (Terminal)
{
return;
}
var protectedBatches = snapshot.OrdinaryUnknownBatches.Concat(new[] { snapshot.Active, snapshot.Unknown }).Where(batch => batch != null);
var removable = operations.Where(operation => snapshot.RemoteOnlyWrites.ContainsKey(operation.Sequence) &&
!protectedBatches.Any(batch => batch.Items.Any(item => item.Key == operation.Key && item.Sequence < operation.Sequence))).ToArray();
if (removable.Length == 0)
{
return;
}
var copy = snapshot.Clone();
foreach (var operation in removable)
{
copy.RemoteOnlyWrites.Remove(operation.Sequence);
}
Commit(copy, status);
}
}
private static Dictionary LocalView(KVSnapshot source)
{
return new Dictionary(source.LocalData, StringComparer.Ordinal);
}
bool Terminal => status == PlayerKVSyncStatus.Closed || status == PlayerKVSyncStatus.StorageError;
PlayerKVStateResult Commit(KVSnapshot copy, PlayerKVSyncStatus nextStatus, long sequence = 0)
{
if (snapshot.Revision == long.MaxValue)
{ status = PlayerKVSyncStatus.StorageError; return new PlayerKVStateResult(PlayerKVChangeError.StorageError, sequence, snapshot.Revision); }
copy.Revision = snapshot.Revision + 1;
if (!store.Save(copy, out var uncertain))
{
status = PlayerKVSyncStatus.StorageError;
return new PlayerKVStateResult(PlayerKVChangeError.StorageError, sequence, copy.Revision, uncertain);
}
snapshot = copy; status = nextStatus;
return new PlayerKVStateResult(PlayerKVChangeError.None, sequence, copy.Revision);
}
static void Apply(Dictionary data, string key, PlayerKVValue value)
{ if (value.Exists) data[key] = value.Value; else data.Remove(key); }
PlayerKVStateResult Blocked() => new PlayerKVStateResult(PlayerKVChangeError.Blocked, revision: snapshot.Revision);
PlayerKVStateResult Invalid() => new PlayerKVStateResult(PlayerKVChangeError.Validation, revision: snapshot.Revision);
public void Dispose()
{
lock (gate)
{
if (status == PlayerKVSyncStatus.Closed) return;
status = PlayerKVSyncStatus.Closed;
ClearComparison();
store.Dispose();
}
}
}
}