# WSX Protocol

**Version**: 1.0.0
**Status**: SOURCE OF TRUTH
**Last Updated**: 2025-12-03

---

## Overview

WSX (WebSocket eXtended) is a **message format** for RPC over WebSocket and NATS.
It brings HTTP-like semantics to message-based transports.

**Important**: WSX is a protocol/format specification.
Request handling uses `MsgRequest(BaseRequest)` from `request.py`.

---

## Motivation

Different transports have different APIs:

- **HTTP**: method, path, headers, cookies, query, body
- **WebSocket**: just binary/text messages
- **NATS**: subject, payload bytes, reply-to

WSX defines a message format that encapsulates HTTP-like semantics:

```
HTTP Request  ─────┐
                   │
WebSocket RPC ─────┼──► BaseRequest ──► Handler ──► BaseResponse
                   │
NATS Message  ─────┘
```

---

## Message Format

### Prefix

WSX messages start with `WSX://` followed by JSON:

```
WSX://{"id":"...","method":"...","path":"...","headers":{},"data":{}}
```

### WSX Request

```json
WSX://{
    "id": "uuid-123",
    "method": "POST",
    "path": "/users/42",
    "headers": {
        "content-type": "application/json",
        "authorization": "Bearer xxx"
    },
    "cookies": {
        "session_id": "xyz-789"
    },
    "query": {
        "limit": "10::L",
        "active": "true::B"
    },
    "data": {
        "name": "Mario",
        "birth": "1990-05-15::D"
    }
}
```

#### Request Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Correlation ID |
| `method` | string | Yes | GET, POST, PUT, DELETE, PATCH |
| `path` | string | Yes | Routing path (e.g., "/users/42") |
| `headers` | object | No | HTTP headers as dict |
| `cookies` | object | No | Cookies as dict |
| `query` | object | No | Query parameters (TYTX supported) |
| `data` | any | No | Request payload (TYTX supported) |

### WSX Response

```json
WSX://{
    "id": "uuid-123",
    "status": 200,
    "headers": {
        "content-type": "application/json",
        "x-request-id": "trace-456"
    },
    "cookies": {
        "session_id": {
            "value": "new-xyz",
            "max_age": "3600::L",
            "httponly": "true::B"
        }
    },
    "data": {
        "id": "42::L",
        "message": "User created"
    }
}
```

#### Response Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Same correlation ID as request |
| `status` | int | Yes | HTTP status code (200, 404, 500, etc.) |
| `headers` | object | No | Response headers |
| `cookies` | object | No | Set-Cookie equivalents |
| `data` | any | No | Response payload |

---

## TYTX Integration

Values support TYTX type suffixes for type preservation:

```
"price": "99.50::N"     → Decimal("99.50")
"date": "2025-01-15::D" → date(2025, 1, 15)
"count": "42::L"        → 42 (int)
"active": "true::B"     → True (bool)
```

WSX parsing automatically hydrates these values via `from_tytx()`.

---

## Transport Flow

### WebSocket (ASGI)

```
1. WebSocket receive() → text message
2. Detect WSX:// prefix or ::JS marker
3. Parse via from_tytx() → hydrates TYTX values automatically
4. Create MsgRequest(BaseRequest)
5. Handler processes → result
6. WsResponse.send() → to_tytx() → WebSocket send()
```

### NATS (Future)

```
1. NATS subscribe callback → msg
2. msg.data contains WSX://... or ::JS marker
3. Parse via from_tytx() → hydrates TYTX values automatically
4. Create NatsRequest(BaseRequest)
5. Handler processes → result
6. NatsResponse.send() → to_tytx() → nc.publish(msg.reply, ...)
```

---

## Module Structure

The `wsx/` directory contains **protocol** code only:

```
src/genro_asgi/wsx/
├── __init__.py
├── protocol.py      # Parse/serialize WSX messages
└── handler.py       # Route WSX messages to handlers
```

**Request classes are in `request.py`**, not in `wsx/`.

---

## Protocol Parsing

```python
# wsx/protocol.py - using genro-tytx API

WSX_PREFIX = "WSX://"

def parse_wsx_message(raw: str | bytes) -> dict[str, Any]:
    """
    Parse WSX message string.

    Args:
        raw: Raw message (WSX:// prefix or ::JS marker)

    Returns:
        Parsed message dict with hydrated TYTX values

    Raises:
        ValueError: If not a valid WSX message
    """
    if isinstance(raw, bytes):
        # Binary data - try msgpack via from_tytx
        from genro_tytx import from_tytx
        return dict(from_tytx(raw, transport="msgpack"))

    # String data
    if raw.startswith(WSX_PREFIX):
        raw = raw[len(WSX_PREFIX):]

    # from_tytx handles ::JS marker and hydration automatically
    from genro_tytx import from_tytx
    return dict(from_tytx(raw))


def serialize_wsx_response(
    request_id: str,
    status: int,
    data: Any,
    headers: dict[str, str] | None = None,
    cookies: dict[str, Any] | None = None,
) -> str:
    """
    Serialize response to WSX format.

    Returns:
        JSON string with ::JS marker (handled by to_tytx)
    """
    from genro_tytx import to_tytx

    response = {
        "id": request_id,
        "status": status,
        "data": data,
    }
    if headers:
        response["headers"] = headers
    if cookies:
        response["cookies"] = cookies

    # to_tytx includes ::JS marker automatically
    return to_tytx(response)
```

---

## Error Handling

Errors returned as response with status:

```json
WSX://{
    "id": "uuid-123",
    "status": 404,
    "data": {
        "error": "User not found",
        "code": "USER_NOT_FOUND"
    }
}
```

---

## Streaming

For streaming responses:
- Multiple WSX responses with same `id`
- `"stream": true` indicates more messages follow
- `"stream": false` or absent indicates final message

```json
WSX://{"id": "123", "status": 200, "data": {"chunk": 1}, "stream": true}
WSX://{"id": "123", "status": 200, "data": {"chunk": 2}, "stream": true}
WSX://{"id": "123", "status": 200, "data": {"chunk": 3}, "stream": false}
```

---

## Correlation ID

- **HTTP**: Generated by server or from `x-request-id` header
- **WebSocket**: Required in WSX message `id` field
- **NATS**: Uses native reply-to for routing, `id` for application tracing

---

## Transport-Agnostic Handler

```python
async def get_user(request: BaseRequest) -> dict:
    """Works with HTTP, WebSocket, or NATS."""
    user_id = request.path.split("/")[-1]
    auth = request.headers.get("authorization")
    session = request.cookies.get("session_id")

    user = await db.get_user(user_id)
    return {
        "id": user.id,
        "name": user.name,
        "transport": request.transport,  # "http", "websocket", or "nats"
    }
```

---

**Copyright**: Softwell S.r.l. (2025)
**License**: Apache License 2.0
