packages feed

gbnet-hs-0.1.1.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
- **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
- **Connection migration** — Seamless IP address change handling

---

## Quick Start

Add to your `.cabal` file:

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

### Simple Game Loop

```haskell
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, peer') <- peerTick outgoing peer

  -- Handle events
  liftIO $ mapM_ handleEvent events

  gameLoop peer'

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 peer' = 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

```haskell
let config = defaultNetworkConfig
      { ncMaxClients = 32
      , ncConnectionTimeoutMs = 10000.0
      , ncKeepaliveIntervalMs = 1000.0
      , ncMtu = 1200
      , ncEnableConnectionMigration = True
      , ncChannelConfigs = [unreliableChannel, reliableChannel]
      }
```

---

## 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
```

---

## 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.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)
```

---

## 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, acc') = drainTop 1200 entitySize acc
```

### Snapshot Interpolation

Smooth client-side rendering:

```haskell
import GBNet.Replication.Interpolation

let buffer' = pushSnapshot serverTime state buffer
case sampleSnapshot renderTime buffer' 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
case peerStats peerId peer of
  Nothing -> pure ()  -- Peer not connected
  Just stats -> case 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)
```

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

### 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

### 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>