From c1c1792dc595ca4492c196b0324431a3657bf909 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 20 Jul 2026 03:05:28 +0100 Subject: [PATCH] Serve provider-owned content chunks directly --- .../CultMeshTcpContentTransport.cs | 25 ++++++++++++++++--- .../CultMeshContentSessionTests.cs | 10 +++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/GameCult.Mesh/CultMeshTcpContentTransport.cs b/src/GameCult.Mesh/CultMeshTcpContentTransport.cs index f70f2a34..0c3eb19e 100644 --- a/src/GameCult.Mesh/CultMeshTcpContentTransport.cs +++ b/src/GameCult.Mesh/CultMeshTcpContentTransport.cs @@ -210,7 +210,7 @@ namespace GameCult.Mesh public sealed class CultMeshTcpContentServer : IDisposable { private readonly TcpListener _listener; - private readonly CultCache _content; + private readonly Func _resolveChunk; private readonly Func _canServeHash; private readonly CancellationTokenSource _shutdown = new(); private readonly ConcurrentDictionary _clients = new(); @@ -222,9 +222,22 @@ namespace GameCult.Mesh TcpListener listener, CultCache content, Func? canServeHash = null) + : this(listener, CreateCacheResolver(content), canServeHash) + { + } + + /// + /// Starts a dedicated TCP content server over provider-owned immutable chunks. + /// The resolver receives a normalized SHA-256 hash and may return + /// when that chunk is unavailable. + /// + public CultMeshTcpContentServer( + TcpListener listener, + Func resolveChunk, + Func? 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(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 CreateCacheResolver(CultCache content) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + return hash => content.Get(CultMeshCdnArtifactChunk.CreateRecordKey(hash)); + } } internal static class CultMeshTcpContentWire diff --git a/tests/GameCult.Mesh.Tests/CultMeshContentSessionTests.cs b/tests/GameCult.Mesh.Tests/CultMeshContentSessionTests.cs index 3dda96cd..6bf5e88b 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshContentSessionTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshContentSessionTests.cs @@ -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) });