F3: an override drifts exactly as the same member would on a plain record. A slot the type dropped is ignored and reported by the store's schema resolution (LastSchemaMigrationReports), the one surface a plain record uses; a value that no longer decodes refuses the load, naming the variant and member. The second report surface (VariantMigrationReports, the per-variant report map, the catalog slot-name plumbing) is deleted. F4: an override for a slot the type does not have, or a value that does not decode as the member, is refused at write. Drift is for type changes after the fact, not for bad writes. F5: two overrides of one slot are refused. F2: a variant that shares an indexed value with another record is refused, naming both keys and the index (the R6 rule extended to [CultIndex]); plain record duplicate handling is unchanged. F7: a flatten now resolves against the same batch, so a base edited in that batch is in the flattened record; and the directory store refuses to load a variant page as it refuses to write one. The C1 vector's big variant now overrides its indexed code, as the unique-index rule requires. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
91 lines
3.6 KiB
C#
91 lines
3.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace GameCult.Caching
|
|
{
|
|
// The store's encoding, as the cache needs it: the one pair CultInspectorModel and variant resolution share.
|
|
// Serialize takes any value, not only documents, under its owning document type's options; Deserialize is its
|
|
// inverse (a document type is checked against the registry, a member type is not); Overlay replaces slots of a
|
|
// serialized document payload with already-encoded member values and touches nothing else.
|
|
public sealed class CultCodec
|
|
{
|
|
public CultCodec(
|
|
Func<object, Type, Type, byte[]> serialize,
|
|
Func<Type, Type, byte[], object?> deserialize,
|
|
Func<byte[], IReadOnlyList<KeyValuePair<int, byte[]>>, byte[]> overlay)
|
|
{
|
|
Serialize = serialize ?? throw new ArgumentNullException(nameof(serialize));
|
|
Deserialize = deserialize ?? throw new ArgumentNullException(nameof(deserialize));
|
|
Overlay = overlay ?? throw new ArgumentNullException(nameof(overlay));
|
|
}
|
|
|
|
// (value, value type, owning document type)
|
|
public Func<object, Type, Type, byte[]> Serialize { get; }
|
|
|
|
// (value type, owning document type, bytes)
|
|
public Func<Type, Type, byte[], object?> Deserialize { get; }
|
|
|
|
// (document payload, slot and encoded value pairs)
|
|
public Func<byte[], IReadOnlyList<KeyValuePair<int, byte[]>>, byte[]> Overlay { get; }
|
|
}
|
|
|
|
public enum CultOverrideOp
|
|
{
|
|
Set = 0,
|
|
Insert = 1,
|
|
Remove = 2
|
|
}
|
|
|
|
// One step of a member path: the MessagePack slot, and the id of the object-list element the step enters ("" when it
|
|
// does not enter one).
|
|
public sealed class CultPathStep
|
|
{
|
|
public CultPathStep(int slot, string elementId = "")
|
|
{
|
|
Slot = slot;
|
|
ElementId = elementId ?? string.Empty;
|
|
}
|
|
|
|
public int Slot { get; }
|
|
public string ElementId { get; }
|
|
|
|
public override string ToString() => ElementId.Length == 0 ? $"[{Slot}]" : $"[{Slot}:{ElementId}]";
|
|
}
|
|
|
|
// The wire shape [op, path[], id, value]; value is the override's already-encoded MessagePack value.
|
|
public sealed class CultVariantOverride
|
|
{
|
|
public CultVariantOverride(CultOverrideOp op, IReadOnlyList<CultPathStep> path, string id, byte[] value)
|
|
{
|
|
Op = op;
|
|
Path = path ?? throw new ArgumentNullException(nameof(path));
|
|
Id = id ?? string.Empty;
|
|
Value = value ?? throw new ArgumentNullException(nameof(value));
|
|
}
|
|
|
|
public CultOverrideOp Op { get; }
|
|
public IReadOnlyList<CultPathStep> Path { get; }
|
|
public string Id { get; }
|
|
public byte[] Value { get; }
|
|
|
|
// A top-level Set: the whole member at slot takes the encoded value.
|
|
public static CultVariantOverride Set(int slot, byte[] value) =>
|
|
new(CultOverrideOp.Set, new[] { new CultPathStep(slot) }, string.Empty, value);
|
|
|
|
internal string PathText => string.Concat(Path.Select(step => step.ToString()));
|
|
}
|
|
|
|
// What a variant record stores: its base and its overrides. Never the resolved document.
|
|
public sealed class CultVariantDelta
|
|
{
|
|
public CultVariantDelta(string baseKey, IReadOnlyList<CultVariantOverride> overrides)
|
|
{
|
|
BaseKey = baseKey ?? throw new ArgumentNullException(nameof(baseKey));
|
|
Overrides = overrides ?? throw new ArgumentNullException(nameof(overrides));
|
|
}
|
|
|
|
public string BaseKey { get; }
|
|
public IReadOnlyList<CultVariantOverride> Overrides { get; }
|
|
}
|
|
}
|