Skip to main content

Projector Examples

Read-side projectors from the poker domain. All code is from the actual examples/ directory.

Projectors subscribe to events and produce read-optimized views: text logs, database tables, search indexes, or external API calls.


Implementation Styles

Angzarr supports two projector implementation styles:

StyleDescriptionBest For
OO (Object-Oriented)Projector class with @projects/[Projects] decoratorsRich state, encapsulation
FunctionalStateRouter or ProjectorHandler with function handlersSimple projections, stateless transforms
LanguageOOFunctional
Python
C#
Rust
Java
Go
C++

Output Projector

The Output Projector subscribes to events from multiple domains (player, table, hand) and writes formatted game logs to a file. This demonstrates a multi-domain projector.

examples/python/prj-output-oo/main.py

class OutputProjector(Projector):
"""Output projector using OO-style decorators."""

name = "output"
input_domains = ["player", "table", "hand"]

@projects(player.PlayerRegistered)
def project_registered(self, event: player.PlayerRegistered) -> types.Projection:
write_log(f"PLAYER registered: {event.display_name} ({event.email})")
return types.Projection(projector=self.name)

@projects(player.FundsDeposited)
def project_deposited(self, event: player.FundsDeposited) -> types.Projection:
amount = event.amount.amount if event.HasField("amount") else 0
new_balance = event.new_balance.amount if event.HasField("new_balance") else 0
write_log(f"PLAYER deposited {amount}, balance: {new_balance}")
return types.Projection(projector=self.name)

@projects(table.TableCreated)
def project_table_created(self, event: table.TableCreated) -> types.Projection:
write_log(f"TABLE created: {event.table_name} ({event.game_variant})")
return types.Projection(projector=self.name)

@projects(table.PlayerJoined)
def project_player_joined(self, event: table.PlayerJoined) -> types.Projection:
player_id = truncate_id(event.player_root)
write_log(f"TABLE player {player_id} joined with {event.stack} chips")
return types.Projection(projector=self.name)

@projects(table.HandStarted)
def project_hand_started(self, event: table.HandStarted) -> types.Projection:
write_log(
f"TABLE hand #{event.hand_number} started, "
f"{len(event.active_players)} players, dealer at position {event.dealer_position}"
)
return types.Projection(projector=self.name)

@projects(hand.CardsDealt)
def project_cards_dealt(self, event: hand.CardsDealt) -> types.Projection:
write_log(f"HAND cards dealt to {len(event.player_cards)} players")
return types.Projection(projector=self.name)

@projects(hand.BlindPosted)
def project_blind_posted(self, event: hand.BlindPosted) -> types.Projection:
player_id = truncate_id(event.player_root)
write_log(f"HAND player {player_id} posted {event.blind_type} blind: {event.amount}")
return types.Projection(projector=self.name)

@projects(hand.ActionTaken)
def project_action_taken(self, event: hand.ActionTaken) -> types.Projection:
player_id = truncate_id(event.player_root)
write_log(f"HAND player {player_id}: {event.action} {event.amount}")
return types.Projection(projector=self.name)

@projects(hand.PotAwarded)
def project_pot_awarded(self, event: hand.PotAwarded) -> types.Projection:
winners = [
f"{truncate_id(w.player_root)} wins {w.amount}" for w in event.winners
]
write_log(f"HAND pot awarded: {', '.join(winners)}")
return types.Projection(projector=self.name)

@projects(hand.HandComplete)
def project_hand_complete(self, event: hand.HandComplete) -> types.Projection:
write_log(f"HAND #{event.hand_number} complete")
return types.Projection(projector=self.name)



StateRouter Pattern

The StateRouter pattern provides fluent event handler registration with explicit state management. It's the functional alternative to OO projectors:

examples/python/prj-output/output_projector_doc.py

player_names: Dict[str, str] = {}


def handle_player_registered(event: player.PlayerRegistered):
player_names[event.player_id] = event.display_name
print(f"[Player] {event.display_name} registered")


def handle_funds_deposited(event: player.FundsDeposited):
name = player_names.get(event.player_id, event.player_id)
print(f"[Player] {name} deposited ${event.amount.amount / 100:.2f}")


def handle_cards_dealt(event: hand.CardsDealt):
for pc in event.player_cards:
name = player_names.get(pc.player_id, pc.player_id)
print(f"[Hand] {name} dealt cards")


router = (
StateRouter("prj-output")
.subscribes("player", ["PlayerRegistered", "FundsDeposited"])
.subscribes("hand", ["CardsDealt", "ActionTaken", "PotAwarded"])
.on("PlayerRegistered", handle_player_registered)
.on("FundsDeposited", handle_funds_deposited)
.on("CardsDealt", handle_cards_dealt)
)

Multi-Domain Subscription

Projectors can subscribe to events from multiple domains. The Output projector subscribes to player, table, and hand domains:

player domain   ──┐
├──→ [Output Projector] ──→ hand_log.txt
table domain ──┤

hand domain ──┘

Each language's ProjectorHandler/ProjectorBase accepts multiple domain names:

handler = ProjectorHandler("output", "player", "table", "hand")

Projector Principles

  1. Read-only — Projectors never modify domain state, only create read views
  2. Idempotent — Same events always produce same projections
  3. Catchup safe — Can replay full event history to rebuild state
  4. Domain aware — Subscribe to specific domains, filter unwanted events
  5. Stateful OK — Can maintain local state (caches, maps) for rendering

Running Projectors

With ProjectorHandler (All Languages)

# Python
cd examples/python && python -m prj-output.main

# Go
cd examples/go && go run ./prj-output

# Rust
cd examples/rust && cargo run --bin prj-output

# Java
cd examples/java && ./gradlew prj-output:run

# C#
cd examples/csharp && dotnet run --project Prj/Output

# C++
cd examples/cpp && ./build/prj-output

Next Steps