Serve provider-owned content chunks directly

This commit is contained in:
Meta Vens 2026-07-20 03:05:28 +01:00
commit c1c1792dc5
2 changed files with 28 additions and 7 deletions

View file

@ -210,7 +210,7 @@ namespace GameCult.Mesh
public sealed class CultMeshTcpContentServer : IDisposable
{
private readonly TcpListener _listener;
private readonly CultCache _content;
private readonly Func<string, CultMeshCdnArtifactChunk?> _resolveChunk;
private readonly Func<string, bool> _canServeHash;
private readonly CancellationTokenSource _shutdown = new();
private readonly ConcurrentDictionary<TcpClient, byte> _clients = new();
@ -222,9 +222,22 @@ namespace GameCult.Mesh
TcpListener listener,
CultCache content,
Func<string, bool>? canServeHash = null)
: this(listener, CreateCacheResolver(content), canServeHash)
{
}
/// <summary>
/// Starts a dedicated TCP content server over provider-owned immutable chunks.
/// The resolver receives a normalized SHA-256 hash and may return <see langword="null"/>
/// when that chunk is unavailable.
/// </summary>
public CultMeshTcpContentServer(
TcpListener listener,
Func<string, CultMeshCdnArtifactChunk?> resolveChunk,
Func<string, bool>? canServeHash = null)
{
_listener = listener ?? throw new ArgumentNullException(nameof(listener));
_content = content ?? throw new ArgumentNullException(nameof(content));
_resolveChunk = resolveChunk ?? throw new ArgumentNullException(nameof(resolveChunk));
_canServeHash = canServeHash ?? (_ => true);
_listener.Start();
_acceptLoop = AcceptLoopAsync(_shutdown.Token);
@ -306,7 +319,7 @@ namespace GameCult.Mesh
if (!string.IsNullOrWhiteSpace(request.RecordKey) &&
!string.Equals(request.RecordKey, key.Value, StringComparison.Ordinal))
throw new InvalidDataException("Content chunk record key disagrees with its content hash.");
chunk = _content.Get<CultMeshCdnArtifactChunk>(key)
chunk = _resolveChunk(hash)
?? throw new FileNotFoundException("Content chunk is not available.", key.Value);
CultMeshCdn.ValidateChunkPayload(new CultMeshCdnChunkRef
{
@ -335,6 +348,12 @@ namespace GameCult.Mesh
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
}
private static Func<string, CultMeshCdnArtifactChunk?> CreateCacheResolver(CultCache content)
{
if (content == null) throw new ArgumentNullException(nameof(content));
return hash => content.Get<CultMeshCdnArtifactChunk>(CultMeshCdnArtifactChunk.CreateRecordKey(hash));
}
}
internal static class CultMeshTcpContentWire

View file

@ -34,12 +34,14 @@ public sealed class CultMeshContentSessionTests
{
var payload = Enumerable.Range(0, 700_000).Select(value => (byte)(value % 251)).ToArray();
var artifact = CultMesh.PackCdnArtifact("aetheria/world/windows", payload);
using var providerCache = new CultCache(CultMesh.CreateCultCacheDocumentRegistry(
typeof(CultMeshCdnArtifactManifest), typeof(CultMeshCdnArtifactChunk)));
await CultMeshCdn.PublishAsync(providerCache, artifact);
var providerChunks = artifact.Chunks.ToDictionary(
chunk => (chunk.ChunkHash.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
? chunk.ChunkHash.Substring("sha256:".Length)
: chunk.ChunkHash).Trim().ToLowerInvariant(),
StringComparer.Ordinal);
using var contentServer = new CultMeshTcpContentServer(
new TcpListener(IPAddress.Loopback, 0),
providerCache);
hash => providerChunks.TryGetValue(hash, out var chunk) ? chunk : null);
var endpoint = $"{CultMeshTcpContentTransportConnector.Scheme}://127.0.0.1:{contentServer.LocalEndPoint.Port}";
var connector = new CultMeshTcpContentTransportConnector();
using var discovery = new CultMeshDiscoveryService(new[] { new RouteSource(endpoint) });