Skip to main content

Python reactive state management framework with server/client incremental sync

Project description

ReactPie

ReactPie

Reactive state management for Python with server/client incremental sync.

PyPI version Python License: MIT Tests

English | 中文


Introduction

ReactPie makes server-side data mutations behave like slicing a pie — each attribute change or container operation produces a precise incremental delta, serialized into a compact binary protocol and pushed over WebSocket in real time. Clients automatically reconstruct a fully consistent reactive mirror of the game state.

Zero external dependencies. Pure Python standard library.

Features

  • Field-level deltas — generated at mutation time, no post-hoc diffing
  • Reactive containersRList / RDict / RSet wrap native containers, mutations sync automatically
  • Binary protocol — custom TAG + zigzag + LEB128 encoding, more compact than JSON
  • Lifecycle management — reference-counted connect / disconnect / die
  • Declarative callbackswatch() for field changes, on_create / on_delete for entity lifecycle

Installation

pip install reactpie

Development mode:

git clone https://github.com/Darksky/ai-game-arena
cd ai-game-arena/python-package/reactpie
pip install -e ".[dev]"

Quick Start

import asyncio
from reactpie import ReactServer, ReactClient, RClass, reactive, watch

# ── Define a data model ──

@router
class User(RClass):
    name = reactive(str, default="")
    hp = reactive(int, default=100)

    def __rinit__(self, name: str = ""):
        self.name = name

# ── Server ──

server = ReactServer()
server.include_type(User)
server.append(router)

client = ReactClient()
client.include_type(User)

# ── Client-side listeners ──

@client.on_create(User)
def on_create(instance: User):
    print(f"Player created: {instance.name}")

    @watch(instance, "hp")
    def on_hp_change(inst, delta):
        print(f"{inst.name} HP: {inst.hp}")

# ── Run ──

async def main():
    async def runner():
        async for packet in server.serve():
            client.push(packet)

    task = asyncio.create_task(runner())

    user = User("Alice")
    server.collect(user)
    user.hp -= 20    # client automatically receives the HP delta

    await asyncio.sleep(1)
    task.cancel()
    await server.wait_stop()

asyncio.run(main())

Modules

Module Responsibility
reactpie.core Reactive base classes, serialization protocol, watch(), context & routing
reactpie.server ReactServer — instant/batch push modes, serve() async generator
reactpie.client ReactClient — deserialization, state mirroring, lifecycle callbacks

API Overview

RClass — Reactive Data Model

from reactpie import RClass, reactive, reactive_self

class Player(RClass):
    hp = reactive(int, default=100)
    name = reactive(str, default="")
    inventory = reactive(list, default=[])   # auto-wrapped as RList
    target = reactive_self()                 # self-referencing field

    def __rinit__(self, name: str = ""):     # replaces __init__
        self.name = name

    def __post_load__(self):                 # called after client-side reconstruction
        ...

watch — Change Listeners

from reactpie import watch, DeltaType

watch(obj, callback=cb)                          # listen to all deltas
watch(obj, callback=cb, catch=DeltaType.UPDATE)  # filter by delta type
watch(obj, callback=cb, field="hp")              # filter by field name
watch(obj, callback=cb, depth=0)                 # filter by depth

@watch(obj, field="x")                           # decorator form
def on_x_change(instance, delta):
    ...

ReactServer / ReactClient

# Server
server = ReactServer()
server.include_type(MyModel)
server.collect(root_instance)             # mark as root, triggers client on_create

async for packet in server.serve():       # yields MultiDelta bytes
    await websocket.send(packet)

# Client
client = ReactClient()
client.include_type(MyModel)

@client.on_create(MyModel)               # fires when a ROOT delta arrives
def handle(instance): ...

@client.on_delete(MyModel)               # fires when refcount drops to zero
def handle(instance): ...

client.push(raw_bytes)                    # deserialize and apply

Reactive Containers

from reactpie import RList, RDict, RSet

party = RList()
party.append(alice)     # → reactive_connect, live count +1
party.remove(alice)     # → reactive_disconnect, live count -1
party.clear()           # → disconnect all elements

# RDict, RSet work the same way

Binary Protocol

MultiDelta packet format:
┌──────────┬──────────┬───────────┬─────────────────────┐
│ Magic (2)│ Ver  (1) │ Count (2) │  Delta × Count      │
│ "RD"     │  0x01    │ u16 LE    │                     │
└──────────┴──────────┴───────────┴─────────────────────┘
DeltaType Description
UPDATE Field assignment
ROOT / CREATE Instance creation (ROOT triggers on_create, CREATE registers only)
APPEND / EXTEND / INSERT List append operations
REMOVE / POP / CLEAR Container removal
SET Index/key assignment

Examples

Example Description
01_simple_e2e.py Single-file end-to-end via asyncio.Queue
game_schema.py Shared data models (Player, Projectile, GameState)
game_server.py WebSocket game server
game_client.py WebSocket game client
run_game.py Single-process launcher
python examples/01_simple_e2e.py    # single-file demo
python examples/run_game.py         # multi-file WebSocket demo

Testing

python -m pytest tests/ -v       # 242 tests

Project Structure

reactpie/
├── reactpie/
│   ├── __init__.py    # public API exports
│   ├── core.py        # framework core
│   ├── server.py      # ReactServer
│   └── client.py      # ReactClient
├── examples/          # example code
├── tests/             # 242 unit tests
├── docs/              # logo & docs
├── pyproject.toml
└── README.md

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

reactpie-0.1.1.tar.gz (41.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

reactpie-0.1.1-py3-none-any.whl (27.7 kB view details)

Uploaded Python 3

File details

Details for the file reactpie-0.1.1.tar.gz.

File metadata

  • Download URL: reactpie-0.1.1.tar.gz
  • Upload date:
  • Size: 41.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for reactpie-0.1.1.tar.gz
Algorithm Hash digest
SHA256 9e9380c21df864552f977426666c712e139308693fc930f30cecda8a069a9381
MD5 1632915f5cded70bb6dfadaf54521768
BLAKE2b-256 da8cf533ada5cc66aedd0f9c920c69ec6749ae3f4acad80b710923a4a2a161c0

See more details on using hashes here.

File details

Details for the file reactpie-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: reactpie-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 27.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for reactpie-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 87029ea8e4ae1fc99e32e8ddbdb265431a46f8df87e7fddcee1c74ff1c954c35
MD5 fa60e08fcf508ad3019ff1c84f4f1eb1
BLAKE2b-256 4a95d7f536774ebe33e686849a18fe6a00f3be088b11b76ee46bb30cee6587ae

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page