packages feed

gbnet-hs-0.2.2.0: README.md

<div align="center">
<h1>gbnet-hs</h1>
<p><strong>Transport-Level Networking for Haskell</strong></p>
<p>Zero-copy Storable serialization. Reliable UDP transport. Effect-abstracted design for pure testing.</p>
<p><a href="#quick-start">Quick Start</a> · <a href="#networking">Networking</a> · <a href="#serialization">Serialization</a> · <a href="#testing">Testing</a> · <a href="#architecture">Architecture</a></p>
<p>

[![CI](https://github.com/gondola-bros-entertainment/gbnet-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/gondola-bros-entertainment/gbnet-hs/actions/workflows/ci.yml)
[![Hackage](https://img.shields.io/hackage/v/gbnet-hs.svg)](https://hackage.haskell.org/package/gbnet-hs)
![Haskell](https://img.shields.io/badge/haskell-GHC%209.6-purple)
![License](https://img.shields.io/badge/license-MIT-blue)

</p>
</div>

---

## What is gbnet-hs?

A transport-level networking library providing:

- **Zero-copy serialization** — Storable-based, C-level speed (14ns per type)
- **Reliable UDP** — Connection-oriented with ACKs, retransmits, and ordering
- **AEAD encryption** — Optional ChaCha20-Poly1305 with anti-replay nonce tracking
- **Unified Peer API** — Same code for client, server, or P2P mesh
- **Effect abstraction** — `MonadNetwork` typeclass enables pure deterministic testing
- **Congestion control** — Dual-layer: binary mode + TCP New Reno window, with application-level backpressure
- **Zero-poll receive** — Dedicated receive thread via GHC IO manager (epoll/kqueue), STM TQueue delivery
- **IPv4 and IPv6** — Automatic address family detection
- **Connection migration** — Seamless IP address change handling

---

## Quick Start

Add to your `.cabal` file:

```cabal
build-depends:
    gbnet-hs
```

### Simple Game Loop

```haskell
{-# LANGUAGE LambdaCase #-}
import GBNet
import Control.Monad.IO.Class (liftIO)

main :: IO ()
main = do
  -- Create peer (binds UDP socket)
  let addr = anyAddr 7777
  now <- getMonoTimeIO
  Right (peer, sock) <- newPeer addr defaultNetworkConfig now

  -- Wrap socket in NetState (starts dedicated receive thread)
  netState <- newNetState sock addr

  -- Run game loop inside NetT IO
  evalNetT (gameLoop peer) netState

gameLoop :: NetPeer -> NetT IO ()
gameLoop peer = do
  -- Single call: receive, process, broadcast, send
  let outgoing = [(ChannelId 0, encodeMyState myState)]
  (events, updated) <- peerTick outgoing peer

  -- Handle events
  liftIO $ mapM_ handleEvent events

  gameLoop updated

handleEvent :: PeerEvent -> IO ()
handleEvent = \case
  PeerConnected pid dir   -> putStrLn $ "Connected: " ++ show pid
  PeerDisconnected pid _  -> putStrLn $ "Disconnected: " ++ show pid
  PeerMessage pid ch msg  -> handleMessage pid ch msg
  PeerMigrated old new    -> putStrLn "Peer address changed"
```

### Connecting to a Remote Peer

```haskell
-- Initiate connection (handshake happens automatically)
let connecting = peerConnect (peerIdFromAddr remoteAddr) now peer

-- The PeerConnected event fires when handshake completes
```

---

## Networking

### The `peerTick` Function

The recommended API for game loops — handles receive, process, and send in one call:

```haskell
peerTick
  :: MonadNetwork m
  => [(ChannelId, ByteString)] -- Messages to broadcast (channel, data)
  -> NetPeer                   -- Current peer state
  -> m ([PeerEvent], NetPeer)  -- Events and updated state
```

### Peer Events

```haskell
data PeerEvent
  = PeerConnected !PeerId !ConnectionDirection  -- Inbound or Outbound
  | PeerDisconnected !PeerId !DisconnectReason
  | PeerMessage !PeerId !ChannelId !ByteString  -- channel, data
  | PeerMigrated !PeerId !PeerId                -- old address, new address
```

### Channel Reliability Modes

```haskell
import GBNet

-- Unreliable: fire-and-forget (position updates)
let unreliable = defaultChannelConfig { ccDeliveryMode = Unreliable }

-- Reliable ordered: guaranteed delivery, in-order (chat, RPC)
let reliable = defaultChannelConfig { ccDeliveryMode = ReliableOrdered }

-- Reliable sequenced: latest-only, drops stale (state sync)
let sequenced = defaultChannelConfig { ccDeliveryMode = ReliableSequenced }
```

### Configuration

All config types have [optics](https://hackage.haskell.org/package/optics) labels via `OverloadedLabels`:

```haskell
{-# LANGUAGE OverloadedLabels #-}
import Optics ((&), (.~), (?~))

let config = defaultNetworkConfig
      & #ncMaxClients .~ 32
      & #ncConnectionTimeoutMs .~ 10000.0
      & #ncKeepaliveIntervalMs .~ 1000.0
      & #ncMtu .~ 1200
      & #ncEnableConnectionMigration .~ True
      & #ncChannelConfigs .~ [unreliableChannel, reliableChannel]
      & #ncEncryptionKey ?~ EncryptionKey myKey  -- optional AEAD encryption
```

---

## Serialization

### Zero-Copy Storable Serialization

```haskell
{-# LANGUAGE TemplateHaskell #-}
import GBNet

data PlayerState = PlayerState
  { psX :: !Float
  , psY :: !Float
  , psHealth :: !Word8
  } deriving (Eq, Show)

deriveStorable ''PlayerState

-- Serialize (14ns, zero-copy)
let bytes = serialize playerState

-- Deserialize
let Right player = deserialize bytes :: Either String PlayerState
```

### Nested Types Just Work

```haskell
data Vec3 = Vec3 !Float !Float !Float
deriveStorable ''Vec3

data Transform = Transform !Vec3 !Float  -- position + rotation
deriveStorable ''Transform

-- Nested types compose via Storable
let bytes = serialize (Transform pos angle)  -- still 14ns
```

### Why Storable?

- **C-level speed** — 14ns serialization via direct memory layout
- **Standard Haskell** — uses base `Storable` typeclass
- **Composable** — nested types work automatically
- **Pure API** — `serialize`/`deserialize` are pure functions

---

## Testing

### Pure Deterministic Testing with TestNet

The `MonadNetwork` typeclass allows swapping real sockets for a pure test implementation:

```haskell
import GBNet
import GBNet.TestNet

-- Run peer logic purely — no actual network IO
testHandshake :: ((), TestNetState)
testHandshake = runTestNet action (initialTestNetState myAddr)
  where
    action = do
      -- Simulate sending
      netSend remoteAddr someData
      -- Advance simulated time (absolute MonoTime in nanoseconds)
      advanceTime (100 * 1000000)  -- 100ms
      -- Check what would be received
      result <- netRecv
      pure ()
```

### Multi-Peer World Simulation

```haskell
import GBNet.TestNet

-- Create a world with multiple peers
let world0 = newTestWorld

-- Run actions for each peer
let (result1, world1) = runPeerInWorld addr1 action1 world0
let (result2, world2) = runPeerInWorld addr2 action2 world1

-- Advance to absolute time and deliver ready packets
let world3 = worldAdvanceTime (100 * 1000000) world2  -- 100ms
```

### Simulating Network Conditions

```haskell
-- Add 50ms latency
simulateLatency 50

-- 10% packet loss
simulateLoss 0.1

-- Packet duplication and out-of-order delivery (optics)
let testCfg = defaultTestNetConfig
      & #tncDuplicateChance  .~ 0.05    -- 5% chance of duplicating packets
      & #tncOutOfOrderChance .~ 0.1     -- 10% chance of reordering
```

---

## Encryption

Optional ChaCha20-Poly1305 AEAD encryption for post-handshake packets:

```haskell
import GBNet

-- Pre-shared key (32 bytes) — both sides must use the same key
let key = EncryptionKey mySharedKey
let config = defaultNetworkConfig { ncEncryptionKey = Just key }

-- Handshake packets remain plaintext; all data packets are encrypted
-- Anti-replay: monotonic nonce counter rejects duplicate/old packets
```

Wire overhead: 24 bytes per encrypted packet (8-byte nonce + 16-byte auth tag).

## IPv6

IPv6 works out of the box — address family is detected automatically:

```haskell
-- IPv4 (existing)
let addr4 = anyAddr 7777

-- IPv6
let addr6 = anyAddr6 7777           -- bind to [::]:7777
let local = localhost6 7777          -- bind to [::1]:7777
let custom = ipv6 (0,0,0,1) 7777    -- specific address
```

---

## Architecture

```
┌─────────────────────────────────────────┐
│           User Application              │
├─────────────────────────────────────────┤
│  GBNet (top-level re-exports)           │
│  import GBNet -- gets everything        │
├─────────────────────────────────────────┤
│  GBNet.Peer                             │
│  peerTick, peerConnect, PeerEvent       │
├─────────────────────────────────────────┤
│  GBNet.Net (NetT transformer)           │
│  Carries socket state for IO            │
├──────────────┬──────────────────────────┤
│  NetT IO     │  TestNet                 │
│  TQueue +    │  (pure, deterministic)   │
│  recv thread │                          │
├──────────────┴──────────────────────────┤
│  GBNet.Class                            │
│  MonadTime, MonadNetwork typeclasses    │
└─────────────────────────────────────────┘
```

### Module Overview

| Module | Purpose |
|--------|---------|
| `GBNet` | Top-level facade — import this for convenience |
| `GBNet.Class` | `MonadTime`, `MonadNetwork` typeclasses |
| `GBNet.Net` | `NetT` monad transformer with receive thread + TQueue |
| `GBNet.Net.IO` | `initNetState` — create real UDP socket and start receive thread |
| `GBNet.Peer` | `NetPeer`, `peerTick`, connection management |
| `GBNet.Crypto` | ChaCha20-Poly1305 AEAD encryption and decryption |
| `GBNet.Security` | CRC32C integrity, rate limiting, connect tokens |
| `GBNet.Congestion` | Dual-layer congestion control and backpressure |
| `GBNet.TestNet` | Pure test network, `TestWorld` for multi-peer |
| `GBNet.Serialize.TH` | `deriveStorable` TH for zero-copy serialization |
| `GBNet.Serialize` | `serialize`/`deserialize` pure functions |

### Explicit Imports (for larger codebases)

```haskell
-- Instead of `import GBNet`, be explicit:
import GBNet.Class (MonadNetwork, MonadTime, MonoTime(..))
import GBNet.Types (ChannelId(..), SequenceNum(..), MessageId(..))
import GBNet.Net (NetT, runNetT, evalNetT)
import GBNet.Net.IO (initNetState)
import GBNet.Peer (NetPeer, peerTick, PeerEvent(..))
import GBNet.Config (NetworkConfig(..), defaultNetworkConfig)
import GBNet.Crypto (EncryptionKey(..), NonceCounter(..))  -- optional encryption
```

---

## Replication Helpers

### Delta Compression

Only send changed fields:

```haskell
import GBNet.Replication.Delta

instance NetworkDelta PlayerState where
  type Delta PlayerState = PlayerDelta
  diff new old = PlayerDelta { ... }
  apply state delta = state { ... }
```

### Interest Management

Filter by area-of-interest:

```haskell
import GBNet.Replication.Interest

let interest = newRadiusInterest 100.0
if relevant interest entityPos observerPos
  then sendEntity entity
  else skip
```

### Priority Accumulator

Fair bandwidth allocation:

```haskell
import GBNet.Replication.Priority

let acc = register npcId 2.0
        $ register playerId 10.0
          newPriorityAccumulator
let (selected, drained) = drainTop 1200 entitySize acc
```

### Snapshot Interpolation

Smooth client-side rendering:

```haskell
import GBNet.Replication.Interpolation

let updated = pushSnapshot serverTime state buffer
case sampleSnapshot renderTime updated of
  Nothing -> waitForMoreSnapshots
  Just interpolated -> render interpolated
```

---

## Congestion Control

gbnet-hs uses a dual-layer congestion control strategy:

### Binary Mode

A send-rate controller that tracks Good/Bad network conditions:

- **Good mode** — additive increase (AIMD): ramps send rate up to 4x base rate
- **Bad mode** — multiplicative decrease: halves current send rate on loss/high RTT
- Adaptive recovery timer with quick re-entry detection (doubles on rapid Good→Bad transitions)

### Window-Based (TCP New Reno)

A cwnd-based controller layered alongside binary mode:

- **Slow Start** — exponential growth until ssthresh
- **Congestion Avoidance** — additive increase per RTT
- **Recovery** — halves cwnd on packet loss (triggered by fast retransmit)
- **Slow Start Restart** — resets stale cwnd after idle periods (RFC 2861)

### Backpressure API

Applications can query congestion pressure and adapt:

```haskell
import Optics (view)

case peerStats peerId peer of
  Nothing -> pure ()  -- Peer not connected
  Just stats -> case view #nsCongestionLevel stats of
    CongestionNone     -> sendFreely
    CongestionElevated -> reduceNonEssential
    CongestionHigh     -> dropLowPriority
    CongestionCritical -> onlySendEssential
```

---

## Build & Test

Requires [GHCup](https://www.haskell.org/ghcup/) with GHC >= 9.6.

```bash
cabal build                              # Build library
cabal test                               # Run all tests
cabal build --ghc-options="-Werror"      # Warnings as errors
cabal haddock                            # Generate docs
```

---

## Performance

Optimized for game networking:

- **Zero-allocation serialization** — Storable-based `poke`/`peek`, 14ns for user types (~70M ops/sec)
- **Zero-allocation packet headers** — direct memory writes, 17ns serialize
- **Nested types same speed** — Storable composition has no overhead
- **Strict fields** with bang patterns throughout
- **GHC flags**: `-O2 -fspecialise-aggressively -fexpose-all-unfoldings`
- **INLINE pragmas** on hot paths
- **Hardware-accelerated CRC32C** via SSE4.2/ARMv8 CRC
- **Zero-poll receive** — dedicated thread blocks on epoll/kqueue, delivers via STM TQueue

### Benchmarks

```
storable/vec3/serialize       18.98 ns   (52M ops/sec)  -- user types
storable/transform/serialize  20.80 ns   (48M ops/sec)  -- nested types
packetheader/serialize        16.49 ns   (60M ops/sec)
packetheader/deserialize      15.95 ns   (62M ops/sec)
crypto/encrypt/64B             1.9 us   (526K ops/sec)  -- ChaCha20-Poly1305
crypto/decrypt/1KB             4.2 us   (238K ops/sec)
```

Run with `cabal bench --enable-benchmarks`.

---

## Features

### Core Transport
- [x] Zero-copy Storable serialization (sub-20ns roundtrips)
- [x] Nested type composition via Storable typeclass
- [x] Template Haskell `deriveStorable` for automatic instances
- [x] Type-safe newtypes (`ChannelId`, `SequenceNum`, `MonoTime`, `MessageId`)
- [x] Reliable/unreliable/sequenced delivery modes
- [x] RTT estimation and adaptive retransmit
- [x] Large message fragmentation
- [x] Connection migration
- [x] Hardware-accelerated CRC32C validation (SSE4.2/ARMv8/software fallback)
- [x] Self-cleaning rate limiter
- [x] ChaCha20-Poly1305 AEAD encryption with anti-replay
- [x] IPv4 and IPv6 support

### Congestion Control
- [x] Binary mode (Good/Bad with AIMD recovery)
- [x] TCP New Reno window-based control (slow start, avoidance, recovery)
- [x] Slow Start Restart for idle connections (RFC 2861)
- [x] Application-level backpressure via `CongestionLevel`
- [x] CWND loss signal from fast retransmit
- [x] Adaptive recovery timer with quick re-entry detection

### Effect Abstraction
- [x] `MonadNetwork` typeclass
- [x] `NetT` monad transformer with dedicated receive thread + STM TQueue
- [x] `TestNet` pure deterministic network
- [x] `TestWorld` multi-peer simulation
- [x] TestNet packet duplication and out-of-order simulation

### Replication Helpers
- [x] Delta compression
- [x] Interest management
- [x] Priority accumulator
- [x] Snapshot interpolation

---

## Contributing

```bash
cabal test && cabal build --ghc-options="-Werror"
```

---

<p align="center">
  <sub>MIT License · <a href="https://github.com/gondola-bros-entertainment">Gondola Bros Entertainment</a></sub>
</p>