diff --git a/frameworks/shrike-minima/.dockerignore b/frameworks/shrike-minima/.dockerignore
new file mode 100644
index 00000000..6a46dd93
--- /dev/null
+++ b/frameworks/shrike-minima/.dockerignore
@@ -0,0 +1,3 @@
+bin/
+obj/
+.git
diff --git a/frameworks/shrike-minima/.gitignore b/frameworks/shrike-minima/.gitignore
new file mode 100644
index 00000000..cd42ee34
--- /dev/null
+++ b/frameworks/shrike-minima/.gitignore
@@ -0,0 +1,2 @@
+bin/
+obj/
diff --git a/frameworks/shrike-minima/ABI/Native.cs b/frameworks/shrike-minima/ABI/Native.cs
new file mode 100644
index 00000000..92944f9e
--- /dev/null
+++ b/frameworks/shrike-minima/ABI/Native.cs
@@ -0,0 +1,297 @@
+// ReSharper disable always CheckNamespace
+// ReSharper disable always SuggestVarOrType_BuiltInTypes
+// (var is avoided intentionally in this project so that concrete types are visible at call sites.)
+// ReSharper disable always StackAllocInsideLoop
+// ReSharper disable always ClassCannotBeInstantiated
+#pragma warning disable CA2014
+
+namespace Shrike;
+
+///
+/// Linux interop surface for a high-performance, epoll-driven TCP server.
+///
+/// Design goals:
+/// - **Minimal marshaling overhead**: prefer blittable types (e.g., pointers, ints).
+/// - **Explicit error handling**: all functions are marked .
+/// Use Marshal.GetLastPInvokeError() immediately after a failure to read errno.
+/// - **Unsafe-friendly**: exposes pointer overloads for zero-copy recv/send.
+///
+/// Platform notes:
+/// - Constants can differ across libc/architectures/kernels. The values here target
+/// mainstream Linux/glibc on x86_64. If you target other distros/architectures, verify
+/// these values against system headers (bits/socket.h, fcntl.h, sys/epoll.h, sys/eventfd.h).
+/// - Network byte order: ports must be big-endian (use htons); addresses must be set appropriately.
+/// - SIGPIPE: either ignore SIGPIPE process-wide or pass to send.
+///
+internal static unsafe class Native
+{
+ // =========================
+ // P/Invoke
+ // =========================
+
+ ///
+ /// Create a socket. Typically domain=AF_INET, type=SOCK_STREAM, protocol=IPPROTO_TCP.
+ /// Returns a file descriptor (>= 0) on success, or -1 on error (check errno).
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int socket(int domain, int type, int protocol);
+
+ ///
+ /// Bind a socket to an address/port. Use for IPv4.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int bind(int sockfd, ref sockaddr_in addr, uint addrlen);
+
+ ///
+ /// Mark a bound socket as passive (accept incoming connections).
+ /// is the kernel queue length hint.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int listen(int sockfd, int backlog);
+
+ ///
+ /// Accept a new connection. flags can include and
+ /// to atomically configure the accepted FD. Returns new client FD or -1 on error.
+ /// Use Marshal.GetLastPInvokeError() to check for / in edge-triggered loops.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int accept4(int sockfd, IntPtr addr, IntPtr addrlen, int flags);
+
+ ///
+ /// Set a socket option (int value). Common options: , TCP_NODELAY, etc.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int setsockopt(int sockfd, int level, int optname, ref int optval, uint optlen);
+
+ ///
+ /// Set SO_LINGER using struct.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int setsockopt(int sockfd, int level, int optname, ref Linger optval, uint optlen);
+
+ ///
+ /// File control. Typical usage: get/set O_NONBLOCK on a socket.
+ /// Returns result per command, or -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int fcntl(int fd, int cmd, int arg);
+
+ ///
+ /// Close a file descriptor (socket or epoll/eventfd). Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int close(int fd);
+
+ ///
+ /// Read from a file descriptor into unmanaged memory.
+ /// For sockets, prefer .
+ /// Returns bytes read (>=0) or -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long read(int fd, IntPtr buf, ulong count);
+
+ ///
+ /// Write to a file descriptor from unmanaged memory.
+ /// For sockets, prefer .
+ /// Returns bytes written (>=0) or -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long write(int fd, IntPtr buf, ulong count);
+
+ ///
+ /// Receive from a socket into unmanaged memory. Returns bytes received (>=0), 0 on orderly shutdown, or -1 on error.
+ /// Set flags to 0 for normal reads.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long recv(int sockfd, IntPtr buf, ulong len, int flags);
+
+ ///
+ /// Receive from a socket into a raw pointer. Equivalent to the IntPtr overload, but avoids extra pinning overhead when you already have a pointer.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long recv(int sockfd, byte* buf, ulong len, int flags);
+
+ ///
+ /// Send to a socket from unmanaged memory. Returns bytes sent (>=0) or -1 on error.
+ /// Consider passing in flags to avoid SIGPIPE on closed peers.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long send(int sockfd, IntPtr buf, ulong len, int flags);
+
+ ///
+ /// Send to a socket from a raw pointer (long length).
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern long send(int sockfd, byte* buf, long len, int flags);
+
+ ///
+ /// Send to a socket from a raw void* and nuint length.
+ /// This signature maps closely to the native prototype and can reduce marshaling overhead in hot paths.
+ ///
+ [DllImport("libc", SetLastError = true)] public static extern nint send(int sockfd, void* buf, nuint len, int flags);
+
+ ///
+ /// Create an epoll instance. Returns an epoll file descriptor (>=0) or -1 on error.
+ /// Use to set close-on-exec at creation time.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int epoll_create1(int flags);
+
+ ///
+ /// Control the epoll interest list (add/mod/del). The ev points to an epoll_event struct in unmanaged memory.
+ /// Returns 0 on success, -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int epoll_ctl(int epfd, int op, int fd, IntPtr ev);
+
+ ///
+ /// Wait for events. events points to a contiguous array of epoll_event (maxevents elements).
+ /// Returns number of events (>=0) or -1 on error. Use timeout < 0 to block indefinitely.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int epoll_wait(int epfd, IntPtr events, int maxevents, int timeout);
+
+ ///
+ /// Create an eventfd (userspace semaphore/notification). Great for waking worker threads from another thread.
+ /// Returns fd (>=0) or -1 on error.
+ ///
+ [DllImport("libc", SetLastError = true)] internal static extern int eventfd(uint initval, int flags);
+
+ [DllImport("libc", SetLastError = true)] internal static extern int sched_setaffinity(int pid, IntPtr cpusetsize, ref ulong mask);
+
+ [DllImport("libc", SetLastError = true)] internal static extern int sched_setaffinity(int pid, IntPtr cpusetsize, ref cpu_set_t mask);
+
+ [DllImport("libc")] internal static extern int gettid(); // Linux thread id
+
+ // =========================
+ // Struct definitions
+ // =========================
+
+ ///
+ /// IPv4 address (network byte order).
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct in_addr
+ {
+ ///
+ /// Address in network byte order (big-endian). 0 == INADDR_ANY.
+ ///
+ public uint s_addr;
+ }
+
+ ///
+ /// IPv4 socket address. Must be passed with addrlen = (uint)sizeof(sockaddr_in).
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct sockaddr_in
+ {
+ /// Address family (AF_INET).
+ public ushort sin_family;
+
+ /// Port in network byte order (use htons).
+ public ushort sin_port;
+
+ /// IPv4 address (use INADDR_ANY or a specific address in network byte order).
+ public in_addr sin_addr;
+
+ ///
+ /// Padding to match native layout (8 bytes). Must be present for correct size.
+ /// It need not be initialized for normal usage; the kernel ignores it.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] sin_zero;
+ }
+
+ ///
+ /// linger option for SO_LINGER.
+ /// If l_onoff != 0, close() will block up to l_linger seconds to flush pending data.
+ /// Be careful: enabling linger can cause unexpected blocking on close.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Linger
+ {
+ public int l_onoff;
+ public int l_linger;
+ }
+
+
+ // =========================
+ // Constants
+ // =========================
+ // Socket families/types/protocols
+ internal const int AF_INET = 2;
+ internal const int SOCK_STREAM = 1;
+ internal const int IPPROTO_TCP = 6;
+
+ // setsockopt levels / names
+ internal const int SOL_SOCKET = 1;
+ internal const int SO_REUSEADDR = 2;
+ internal const int SO_REUSEPORT = 15;
+ internal const int SO_LINGER = 13;
+ ///
+ /// TCP_NODELAY (disable Nagle). Linux defines this at level IPPROTO_TCP with optname=1.
+ /// (Kept here as constant=1; use level=IPPROTO_TCP when calling setsockopt.)
+ ///
+ internal const int TCP_NODELAY = 1;
+
+ // fcntl / file status flags
+ internal const int O_NONBLOCK = 0x800; // Verify per-arch.
+ internal const int F_GETFL = 3;
+ internal const int F_SETFL = 4;
+
+ // epoll events
+ internal const int EPOLLIN = 0x001;
+ internal const int EPOLLOUT = 0x004;
+ internal const int EPOLLERR = 0x008;
+ internal const int EPOLLHUP = 0x010;
+ internal const int EPOLLRDHUP = 0x2000;
+ internal const uint EPOLLET = 0x80000000;
+ internal const uint EPOLLONESHOT = 0x40000000;
+
+ // epoll_ctl ops
+ internal const int EPOLL_CTL_ADD = 1;
+ internal const int EPOLL_CTL_DEL = 2;
+ internal const int EPOLL_CTL_MOD = 3;
+
+ // CLOEXEC / NONBLOCK flags (creation-time)
+ /// Close-on-exec for epoll_create1/eventfd. (Verify on your target kernel/arch.)
+ internal const int EPOLL_CLOEXEC = 0x80000;
+
+ ///
+ /// On many Linux systems, SOCK_CLOEXEC is 0x1000000 (not 0x80000).
+ /// Validate this constant on your target platform if you pass it to socket() or accept4().
+ ///
+ internal const int SOCK_CLOEXEC = 0x80000;
+
+ /// Creation-time nonblocking for socket/accept4.
+ internal const int SOCK_NONBLOCK = 0x800;
+
+ // eventfd flags
+ internal const int EFD_NONBLOCK = 0x800;
+ internal const int EFD_CLOEXEC = 0x80000;
+
+ // send/recv flags
+ ///
+ /// Suppress SIGPIPE on send. Alternatively, ignore SIGPIPE process-wide.
+ ///
+ internal const int MSG_NOSIGNAL = 0x4000;
+
+ // Common errno values we branch on in tight loops
+ internal const int EINTR = 4;
+ internal const int EAGAIN = 11;
+ internal const int EWOULDBLOCK = 11;
+ internal const int EPIPE = 32;
+ internal const int ECONNABORTED = 103;
+ internal const int ECONNRESET = 104;
+
+ public static void PinCurrentThreadToCpu(int cpuIndex)
+ {
+ if (cpuIndex < 0 || cpuIndex >= Environment.ProcessorCount)
+ throw new ArgumentOutOfRangeException(nameof(cpuIndex));
+
+ unsafe
+ {
+ var set = new cpu_set_t();
+ int word = cpuIndex / 64;
+ int bit = cpuIndex % 64;
+ set.Bits[word] = 1UL << bit;
+
+ int tid = gettid();
+ int ret = sched_setaffinity(tid, (IntPtr)sizeof(cpu_set_t), ref set);
+ if (ret != 0)
+ throw new InvalidOperationException($"sched_setaffinity failed with errno {Marshal.GetLastPInvokeError()}");
+ }
+ }
+}
+
+internal unsafe struct cpu_set_t
+{
+ public fixed ulong Bits[16]; // 1024 bits (enough for up to 1024 CPUs)
+}
\ No newline at end of file
diff --git a/frameworks/shrike-minima/ABI/ProcessorArchDependant.cs b/frameworks/shrike-minima/ABI/ProcessorArchDependant.cs
new file mode 100644
index 00000000..c8a712b2
--- /dev/null
+++ b/frameworks/shrike-minima/ABI/ProcessorArchDependant.cs
@@ -0,0 +1,149 @@
+// ReSharper disable always CheckNamespace
+// ReSharper disable always SuggestVarOrType_BuiltInTypes
+// (var is avoided intentionally in this project so that concrete types are visible at call sites.)
+// ReSharper disable always StackAllocInsideLoop
+// ReSharper disable always ClassCannotBeInstantiated
+#pragma warning disable CA2014
+
+namespace Shrike;
+
+///
+/// Provides architecture-dependent helpers for low-level socket and epoll interop.
+///
+///
+/// Linux’s struct epoll_event has different binary layouts depending on CPU architecture
+/// (notably 12 bytes on x86/x64 and 16 bytes on most other architectures like ARM/ARM64).
+/// This class exposes constants and helpers to correctly read and write those structures
+/// at runtime based on the process architecture.
+///
+///
+///
+/// These methods are used to serialize and deserialize epoll_event structures directly
+/// into unmanaged buffers when interfacing with epoll_wait, epoll_ctl, and related syscalls.
+///
+///
+internal static unsafe class ProcessorArchDependant
+{
+ // =============================================================================================
+ // Architecture-dependent configuration
+ // =============================================================================================
+
+ ///
+ /// Indicates whether the current platform uses a packed epoll_event layout (12 bytes).
+ ///
+ /// On x86 and x64 (little-endian), the epoll_event structure is packed to 12 bytes.
+ /// On ARM, ARM64, and others, it uses natural 8-byte alignment, resulting in 16 bytes.
+ ///
+ ///
+ internal static readonly bool Packed =
+ RuntimeInformation.ProcessArchitecture == Architecture.X64 ||
+ RuntimeInformation.ProcessArchitecture == Architecture.X86;
+
+ ///
+ /// The size (in bytes) of an epoll_event structure for the current runtime architecture.
+ ///
+ /// Typically 12 bytes for packed x86/x64 layouts and 16 for natural alignment layouts.
+ ///
+ ///
+ internal static readonly int EvSize = Packed ? 12 : 16;
+
+ // =============================================================================================
+ // Struct read/write helpers
+ // =============================================================================================
+
+ ///
+ /// Writes a Linux epoll_event structure into a preallocated unmanaged memory region.
+ ///
+ /// Destination pointer to write the structure into.
+ /// Bitmask of epoll events (e.g. EPOLLIN, EPOLLOUT, EPOLLRDHUP, etc.).
+ /// The file descriptor associated with the event.
+ ///
+ ///
+ /// Layouts by architecture:
+ ///
+ /// - Packed (x86/x64): events @ 0 (4 bytes), data @ 4 (8 bytes)
+ /// - Natural (ARM/others): events @ 0 (4 bytes), padding 4, data @ 8 (8 bytes)
+ ///
+ ///
+ /// Only the lower 32 bits of are stored in the data field.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void WriteEpollEvent(void* dest, uint events, int fd)
+ {
+ if (Packed)
+ {
+ // events @0 (4 bytes), data @4 (8 bytes)
+ *(uint*)dest = events;
+ *(ulong*)((byte*)dest + 4) = (uint)fd; // store fd in low 32 bits
+ }
+ else
+ {
+ // events @0 (4 bytes), pad 4, data @8 (8 bytes)
+ *(uint*)dest = events;
+ *(ulong*)((byte*)dest + 8) = (uint)fd;
+ }
+ }
+
+ ///
+ /// Reads a Linux epoll_event structure from unmanaged memory and extracts its fields.
+ ///
+ /// Pointer to the source buffer containing the epoll_event structure.
+ /// Outputs the event flags (EPOLLIN, EPOLLOUT, etc.).
+ /// Outputs the associated file descriptor.
+ ///
+ /// Reads using the correct layout depending on the flag.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void ReadEpollEvent(void* src, out uint events, out int fd)
+ {
+ if (Packed)
+ {
+ events = *(uint*)src;
+ fd = (int)*(uint*)((byte*)src + 4);
+ }
+ else
+ {
+ events = *(uint*)src;
+ fd = (int)*(uint*)((byte*)src + 8);
+ }
+ }
+
+ // Variations, TODO: Test performance required
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal static void WriteEpollEvent2(void* dest, uint events, int fd)
+ {
+ // Write events (always aligned 4B store)
+ *(uint*)dest = events;
+ // Compute data offset (packed: +4, natural: +8)
+ var data = (byte*)dest + (Packed ? 4 : 8);
+ // Store only low 32 bits of fd and zero the high 32 bits.
+ // Using two 4B stores avoids an unaligned 8B write in the packed layout.
+ *(uint*)data = (uint)fd; // low 32
+ *(uint*)(data + 4) = 0; // high 32
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal static void ReadEpollEvent2(void* src, out uint events, out int fd)
+ {
+ events = *(uint*)src;
+ var data = (byte*)src + (Packed ? 4 : 8);
+ // We only ever wrote the low 32 bits; read exactly those.
+ fd = (int)*(uint*)data;
+ }
+
+ // =============================================================================================
+ // Networking helpers
+ // =============================================================================================
+
+ ///
+ /// Converts a 16-bit unsigned integer from host byte order to network byte order (big-endian).
+ ///
+ /// The value to convert.
+ /// The converted value in network byte order.
+ ///
+ /// Equivalent to the native htons() function from the BSD sockets API.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static ushort Htons(ushort x) =>
+ BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(x) : x;
+}
\ No newline at end of file
diff --git a/frameworks/shrike-minima/Dockerfile b/frameworks/shrike-minima/Dockerfile
new file mode 100644
index 00000000..cda98c97
--- /dev/null
+++ b/frameworks/shrike-minima/Dockerfile
@@ -0,0 +1,12 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+COPY shrike-minima.csproj ./
+RUN dotnet restore
+COPY . .
+RUN dotnet publish -c Release -o /app/out --no-restore
+
+FROM mcr.microsoft.com/dotnet/runtime:10.0
+WORKDIR /app
+COPY --from=build /app/out ./
+EXPOSE 8080
+ENTRYPOINT ["dotnet", "shrike-minima.dll"]
diff --git a/frameworks/shrike-minima/Engine/Connection.cs b/frameworks/shrike-minima/Engine/Connection.cs
new file mode 100644
index 00000000..0aea25e6
--- /dev/null
+++ b/frameworks/shrike-minima/Engine/Connection.cs
@@ -0,0 +1,254 @@
+using System.Threading.Tasks.Sources;
+
+// ReSharper disable always CheckNamespace
+// ReSharper disable always SuggestVarOrType_BuiltInTypes
+#pragma warning disable CA2014
+
+namespace Shrike;
+
+///
+/// Per-connection state with Minima-style IVTS on the read and flush paths.
+///
+/// The epoll worker is the DRIVER: on EPOLLIN it drains recv into
+/// then calls ; on EPOLLOUT
+/// it continues and, when drained, calls .
+/// The per-connection handler loop awaits / ;
+/// because RunContinuationsAsynchronously = false, those continuations run inline
+/// on the worker thread — the handler and the driver are the same thread (cooperative,
+/// single-threaded), exactly like Minima's reactor.
+///
+[SkipLocalsInit]
+public sealed unsafe class Connection : IValueTaskSource, IValueTaskSource, IDisposable
+{
+ public enum FlushResult { Complete, Incomplete, Close }
+
+ // ---- recv window: valid bytes in [Head .. Tail) ----
+ public int Head, Tail;
+ public readonly byte* ReceiveBuffer;
+ private readonly int _inSlabSize;
+
+ // ---- send buffer ----
+ public readonly FixedBufferWriter WriteBuffer;
+
+ // ---- per-request parsed header (no allocations) ----
+ public BinaryH1HeaderData BinaryH1HeaderData { get; set; }
+ public H1HeaderData H1HeaderData { get; set; } = null!;
+
+ // ---- epoll wiring (set when the connection is bound to a live fd) ----
+ public int Fd;
+ public int Ep;
+
+ // ---- read IVTS (result = isClosed) ----
+ private ManualResetValueTaskSourceCore _readSignal = new() { RunContinuationsAsynchronously = true };
+ private int _armed;
+ private int _pending;
+ private int _closed;
+
+ // ---- flush IVTS ----
+ private ManualResetValueTaskSourceCore _flushSignal = new() { RunContinuationsAsynchronously = true };
+ private int _flushArmed;
+
+ /// SPSC ring carrying recv chunks from the worker to the handler.
+ public readonly SpscRing RecvRing = new(1024);
+
+ /// Capacity of the handler-owned parse buffer (ReceiveBuffer).
+ public int InCapacity => _inSlabSize;
+
+ public Connection(int maxConnections, int inSlabSize, int outSlabSize)
+ {
+ _inSlabSize = inSlabSize;
+ ReceiveBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)inSlabSize, 64);
+ WriteBuffer = new FixedBufferWriter((byte*)NativeMemory.AlignedAlloc((nuint)outSlabSize, 64), outSlabSize);
+ }
+
+ ///
+ /// Parse the next complete HTTP request from the recv window into
+ /// and advance past it. Returns false when no
+ /// more complete requests remain, compacting any trailing partial to the front
+ /// so the next recv appends after it. Call in a loop after .
+ ///
+ public bool TryReadRequest()
+ {
+ int idx = 0;
+ ReadOnlySpan headerSpan = FindCrlfCrlf(ReceiveBuffer, Head, Tail, ref idx);
+ if (idx < 0)
+ {
+ Compact();
+ return false;
+ }
+ BinaryH1HeaderData = ExtractBinaryH1HeaderData(headerSpan);
+ Head = idx + 4; // advance past CRLFCRLF
+ return true;
+ }
+
+ public void Compact()
+ {
+ if (Head == 0) return; // nothing consumed — partial already at the front, keep it
+ if (Head < Tail)
+ {
+ int length = Tail - Head;
+ Buffer.MemoryCopy(ReceiveBuffer + Head, ReceiveBuffer, _inSlabSize, length);
+ Head = 0;
+ Tail = length;
+ }
+ else
+ {
+ Head = Tail = 0; // fully consumed
+ }
+ }
+
+ /// Bind to a fresh fd taken from the pool and reset all per-connection state.
+ public void Reset(int fd, int ep)
+ {
+ Fd = fd;
+ Ep = ep;
+ Head = Tail = 0;
+ WriteBuffer.Reset();
+ while (RecvRing.TryDequeue(out byte* leftover, out _)) RecvPool.Return(leftover); // drain stale chunks
+ Volatile.Write(ref _armed, 0);
+ Volatile.Write(ref _pending, 0);
+ Volatile.Write(ref _closed, 0);
+ Volatile.Write(ref _flushArmed, 0);
+ _readSignal.Reset();
+ _flushSignal.Reset();
+ }
+
+ public void Clear() => H1HeaderData?.Clear();
+
+ public bool IsClosed => Volatile.Read(ref _closed) != 0;
+
+ // ============================ READ ============================
+
+ public ValueTask ReadAsync()
+ {
+ if (Volatile.Read(ref _pending) == 1)
+ {
+ Volatile.Write(ref _pending, 0);
+ return new ValueTask(Volatile.Read(ref _closed) != 0);
+ }
+ if (Volatile.Read(ref _closed) != 0)
+ return new ValueTask(true);
+
+ _readSignal.Reset();
+ Volatile.Write(ref _armed, 1);
+
+ // Lost-wakeup guard: data/close may have raced in just before we armed.
+ if (Volatile.Read(ref _pending) == 1 || Volatile.Read(ref _closed) != 0)
+ {
+ Volatile.Write(ref _pending, 0);
+ Volatile.Write(ref _armed, 0);
+ return new ValueTask(Volatile.Read(ref _closed) != 0);
+ }
+ return new ValueTask(this, _readSignal.Version);
+ }
+
+ /// Worker thread: the fd is readable — wake the handler's ReadAsync.
+ /// The worker does NOT recv; the handler calls itself.
+ public void SignalReadable()
+ {
+ if (Interlocked.Exchange(ref _armed, 0) == 1)
+ _readSignal.SetResult(Volatile.Read(ref _closed) != 0);
+ else
+ Volatile.Write(ref _pending, 1);
+ }
+
+ // ============================ FLUSH ============================
+
+ public ValueTask FlushAsync()
+ {
+ FlushResult r = TryFlush();
+ if (r == FlushResult.Complete)
+ return ValueTask.CompletedTask;
+ if (r == FlushResult.Close)
+ {
+ MarkClosed();
+ return ValueTask.CompletedTask; // Serve observes IsClosed and exits
+ }
+
+ // Partial / EAGAIN — wait for the worker to drain EPOLLOUT.
+ _flushSignal.Reset();
+ Volatile.Write(ref _flushArmed, 1);
+ ArmEpollOut();
+ return new ValueTask(this, _flushSignal.Version);
+ }
+
+ /// Non-blocking send of everything staged in .
+ public FlushResult TryFlush()
+ {
+ while (true)
+ {
+ long remaining = WriteBuffer.Tail - WriteBuffer.Head;
+ if (remaining == 0) { WriteBuffer.Reset(); return FlushResult.Complete; }
+
+ byte* head = WriteBuffer.Ptr + WriteBuffer.Head;
+ long n = send(Fd, head, remaining, MSG_NOSIGNAL);
+ if (n > 0)
+ {
+ if (n == remaining) { WriteBuffer.Reset(); return FlushResult.Complete; }
+ WriteBuffer.Head += (int)n;
+ continue;
+ }
+
+ int err = (n == 0) ? EAGAIN : Marshal.GetLastPInvokeError();
+ if (err is EAGAIN or EWOULDBLOCK) return FlushResult.Incomplete;
+ return FlushResult.Close;
+ }
+ }
+
+ /// Worker thread: EPOLLOUT fully drained — wake the handler's FlushAsync.
+ public void CompleteFlush()
+ {
+ if (Interlocked.Exchange(ref _flushArmed, 0) == 1)
+ _flushSignal.SetResult(true);
+ }
+
+ // ============================ CLOSE ============================
+
+ public void MarkClosed()
+ {
+ Volatile.Write(ref _closed, 1);
+ if (Interlocked.Exchange(ref _armed, 0) == 1)
+ _readSignal.SetResult(true);
+ else
+ Volatile.Write(ref _pending, 1);
+
+ if (Interlocked.Exchange(ref _flushArmed, 0) == 1)
+ _flushSignal.SetResult(true);
+ }
+
+ // ========================= epoll arming =========================
+
+ private void ArmEpollOut()
+ {
+ byte* ev = stackalloc byte[EvSize];
+ WriteEpollEvent(ev, EPOLLOUT | EPOLLRDHUP | EPOLLERR | EPOLLHUP | EPOLLET, Fd);
+ epoll_ctl(Ep, EPOLL_CTL_MOD, Fd, (IntPtr)ev);
+ }
+
+ public void ArmEpollIn()
+ {
+ byte* ev = stackalloc byte[EvSize];
+ WriteEpollEvent(ev, EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP | EPOLLET, Fd);
+ epoll_ctl(Ep, EPOLL_CTL_MOD, Fd, (IntPtr)ev);
+ }
+
+ // ===================== IValueTaskSource (read) =====================
+ bool IValueTaskSource.GetResult(short token) => _readSignal.GetResult(token);
+ ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _readSignal.GetStatus(token);
+ void IValueTaskSource.OnCompleted(Action