This commit is contained in:
10
backend/events/broadcaster.py
Normal file
10
backend/events/broadcaster.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
class Broadcaster(Thread):
|
||||
"""Background thread sending periodic notifications."""
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
#push event to all users
|
||||
time.sleep(5) # Send every 5 seconds
|
||||
82
backend/events/sse.py
Normal file
82
backend/events/sse.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import json
|
||||
import queue
|
||||
import uuid
|
||||
from typing import Dict, Any
|
||||
import logging
|
||||
|
||||
from flask import Response
|
||||
|
||||
from events.types.event import Event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Maps user_id → dict of {connection_id: queue}
|
||||
user_queues: Dict[str, Dict[str, queue.Queue]] = {}
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
def get_queue(user_id: str, connection_id: str) -> queue.Queue:
|
||||
"""Get or create a queue for a specific user connection."""
|
||||
if user_id not in user_queues:
|
||||
user_queues[user_id] = {}
|
||||
|
||||
if connection_id not in user_queues[user_id]:
|
||||
user_queues[user_id][connection_id] = queue.Queue()
|
||||
|
||||
return user_queues[user_id][connection_id]
|
||||
|
||||
|
||||
def send_to_user(user_id: str, data: Dict[str, Any]):
|
||||
"""Send data to all connections for a specific user."""
|
||||
logger.info(f"Sending data to {user_id} user quesues are {user_queues.keys()}")
|
||||
logger.info(f"Data: {data}")
|
||||
if user_id in user_queues:
|
||||
logger.info(f"Queued {user_id}")
|
||||
# Format as SSE message once
|
||||
message = f"data: {json.dumps(data)}\n\n".encode('utf-8')
|
||||
|
||||
# Send to all connections for this user
|
||||
for connection_id, q in user_queues[user_id].items():
|
||||
try:
|
||||
q.put(message, block=False)
|
||||
except queue.Full:
|
||||
# Skip if queue is full (connection might be dead)
|
||||
pass
|
||||
|
||||
|
||||
def send_event_to_user(user_id: str, event: Event):
|
||||
"""Send an Event to all connections for a specific user."""
|
||||
send_to_user(user_id, event.to_dict())
|
||||
|
||||
|
||||
def sse_response_for_user(user_id: str):
|
||||
"""Create SSE response for a user connection."""
|
||||
# Generate unique connection ID
|
||||
connection_id = str(uuid.uuid4())
|
||||
user_queue = get_queue(user_id, connection_id)
|
||||
logger.info(f"SSE response for {user_id} user is {user_queue}")
|
||||
|
||||
def generate():
|
||||
try:
|
||||
while True:
|
||||
# Get message from queue (blocks until available)
|
||||
message = user_queue.get()
|
||||
yield message
|
||||
except GeneratorExit:
|
||||
# Clean up when client disconnects
|
||||
if user_id in user_queues and connection_id in user_queues[user_id]:
|
||||
del user_queues[user_id][connection_id]
|
||||
|
||||
# Remove user entry if no connections remain
|
||||
if not user_queues[user_id]:
|
||||
del user_queues[user_id]
|
||||
|
||||
return Response(
|
||||
generate(),
|
||||
mimetype='text/event-stream',
|
||||
headers={
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Accel-Buffering': 'no',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
)
|
||||
21
backend/events/types/child_modified.py
Normal file
21
backend/events/types/child_modified.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
class ChildModified(Payload):
|
||||
OPERATION_ADD = "ADD"
|
||||
OPERATION_EDIT = "EDIT"
|
||||
OPERATION_DELETE = "DELETE"
|
||||
def __init__(self, child_id: str, operation: str):
|
||||
super().__init__({
|
||||
'child_id': child_id,
|
||||
'operation': operation,
|
||||
})
|
||||
|
||||
@property
|
||||
def child_id(self) -> str:
|
||||
return self.get("child_id")
|
||||
|
||||
@property
|
||||
def operation(self) -> str:
|
||||
return self.get("operation")
|
||||
|
||||
27
backend/events/types/child_reward_request.py
Normal file
27
backend/events/types/child_reward_request.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
class ChildRewardRequest(Payload):
|
||||
REQUEST_GRANTED = "GRANTED"
|
||||
REQUEST_CREATED = "CREATED"
|
||||
REQUEST_CANCELLED = "CANCELLED"
|
||||
def __init__(self, child_id, reward_id: str, operation: str):
|
||||
super().__init__({
|
||||
'child_id': child_id,
|
||||
'reward_id': reward_id,
|
||||
'operation': operation
|
||||
})
|
||||
|
||||
@property
|
||||
def child_id(self) -> str:
|
||||
return self.get("child_id")
|
||||
|
||||
@property
|
||||
def reward_id(self) -> str:
|
||||
return self.get("reward_id")
|
||||
|
||||
@property
|
||||
def operation(self) -> str:
|
||||
return self.get("operation")
|
||||
|
||||
|
||||
24
backend/events/types/child_reward_triggered.py
Normal file
24
backend/events/types/child_reward_triggered.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
class ChildRewardTriggered(Payload):
|
||||
def __init__(self, reward_id: str, child_id: str, points: int):
|
||||
super().__init__({
|
||||
'reward_id': reward_id,
|
||||
'child_id': child_id,
|
||||
'points': points
|
||||
})
|
||||
|
||||
@property
|
||||
def reward_id(self) -> str:
|
||||
return self.get("reward_id")
|
||||
|
||||
@property
|
||||
def child_id(self) -> str:
|
||||
return self.get("child_id")
|
||||
|
||||
@property
|
||||
def points(self) -> int:
|
||||
return self.get("points", 0)
|
||||
|
||||
|
||||
20
backend/events/types/child_rewards_set.py
Normal file
20
backend/events/types/child_rewards_set.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
class ChildRewardsSet(Payload):
|
||||
def __init__(self, child_id: str, reward_ids: list[str]):
|
||||
super().__init__({
|
||||
'child_id': child_id,
|
||||
'reward_ids': reward_ids
|
||||
})
|
||||
|
||||
@property
|
||||
def child_id(self) -> str:
|
||||
return self.get("child_id")
|
||||
|
||||
@property
|
||||
def reward_ids(self) -> list[str]:
|
||||
return self.get("reward_ids", [])
|
||||
|
||||
|
||||
|
||||
24
backend/events/types/child_task_triggered.py
Normal file
24
backend/events/types/child_task_triggered.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
class ChildTaskTriggered(Payload):
|
||||
def __init__(self, task_id: str, child_id: str, points: int):
|
||||
super().__init__({
|
||||
'task_id': task_id,
|
||||
'child_id': child_id,
|
||||
'points': points
|
||||
})
|
||||
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
return self.get("task_id")
|
||||
|
||||
@property
|
||||
def child_id(self) -> str:
|
||||
return self.get("child_id")
|
||||
|
||||
@property
|
||||
def points(self) -> int:
|
||||
return self.get("points", 0)
|
||||
|
||||
|
||||
19
backend/events/types/child_tasks_set.py
Normal file
19
backend/events/types/child_tasks_set.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
class ChildTasksSet(Payload):
|
||||
def __init__(self, child_id: str, task_ids: list[str]):
|
||||
super().__init__({
|
||||
'child_id': child_id,
|
||||
'task_ids': task_ids
|
||||
})
|
||||
|
||||
@property
|
||||
def child_id(self) -> str:
|
||||
return self.get("child_id")
|
||||
|
||||
@property
|
||||
def task_ids(self) -> list[str]:
|
||||
return self.get("task_ids", [])
|
||||
|
||||
|
||||
18
backend/events/types/event.py
Normal file
18
backend/events/types/event.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from dataclasses import dataclass, field
|
||||
import time
|
||||
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
type: str
|
||||
payload: Payload
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'type': self.type,
|
||||
'payload': self.payload.data,
|
||||
'timestamp': self.timestamp
|
||||
}
|
||||
15
backend/events/types/event_types.py
Normal file
15
backend/events/types/event_types.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
CHILD_TASK_TRIGGERED = "child_task_triggered"
|
||||
CHILD_TASKS_SET = "child_tasks_set"
|
||||
|
||||
TASK_MODIFIED = "task_modified"
|
||||
REWARD_MODIFIED = "reward_modified"
|
||||
|
||||
CHILD_REWARD_TRIGGERED = "child_reward_triggered"
|
||||
CHILD_REWARDS_SET = "child_rewards_set"
|
||||
CHILD_REWARD_REQUEST = "child_reward_request"
|
||||
|
||||
CHILD_MODIFIED = "child_modified"
|
||||
6
backend/events/types/payload.py
Normal file
6
backend/events/types/payload.py
Normal file
@@ -0,0 +1,6 @@
|
||||
class Payload:
|
||||
def __init__(self, data: dict):
|
||||
self.data = data
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
return self.data.get(key, default)
|
||||
22
backend/events/types/reward_modified.py
Normal file
22
backend/events/types/reward_modified.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
class RewardModified(Payload):
|
||||
OPERATION_ADD = "ADD"
|
||||
OPERATION_EDIT = "EDIT"
|
||||
OPERATION_DELETE = "DELETE"
|
||||
def __init__(self, reward_id: str, operation: str):
|
||||
super().__init__({
|
||||
'reward_id': reward_id,
|
||||
'operation': operation
|
||||
})
|
||||
|
||||
@property
|
||||
def reward_id(self) -> str:
|
||||
return self.get("reward_id")
|
||||
|
||||
@property
|
||||
def operation(self) -> str:
|
||||
return self.get("operation")
|
||||
|
||||
|
||||
22
backend/events/types/task_modified.py
Normal file
22
backend/events/types/task_modified.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from events.types.payload import Payload
|
||||
|
||||
|
||||
class TaskModified(Payload):
|
||||
OPERATION_ADD = "ADD"
|
||||
OPERATION_EDIT = "EDIT"
|
||||
OPERATION_DELETE = "DELETE"
|
||||
def __init__(self, task_id: str, operation: str):
|
||||
super().__init__({
|
||||
'task_id': task_id,
|
||||
'operation': operation
|
||||
})
|
||||
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
return self.get("task_id")
|
||||
|
||||
@property
|
||||
def operation(self) -> str:
|
||||
return self.get("operation")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user