Compare commits
	
		
			No commits in common. "1c1ca14fe75fa00f085194024d60c4cd5e1ae7c1" and "a2a17e5ff9ae96c44a7d15a457fb7a0238a892f4" have entirely different histories.
		
	
	
		
			1c1ca14fe7
			...
			a2a17e5ff9
		
	
		
| @ -1,25 +1,40 @@ | |||||||
| import asyncio | import asyncio | ||||||
| 
 | 
 | ||||||
| from quart import Quart, Response, websocket | from quart import Quart, Response, websocket | ||||||
| from typing import NoReturn |  | ||||||
| 
 | 
 | ||||||
| from glebby.connection_manager import ConnectionManager |  | ||||||
| from glebby.model import Model | from glebby.model import Model | ||||||
| 
 | 
 | ||||||
| connection_manager = ConnectionManager() | model = Model() | ||||||
| model = Model(connection_manager) |  | ||||||
| 
 |  | ||||||
| # Files in ./static are served directly under / instead of under /static |  | ||||||
| app = Quart(__name__, static_url_path='') | app = Quart(__name__, static_url_path='') | ||||||
| 
 | 
 | ||||||
| # Root route should point to ./static/index.html | async def send_outgoing(queue: asyncio.Queue[str]) -> None: | ||||||
|  |     while True: | ||||||
|  |         data = await queue.get() | ||||||
|  |         await websocket.send(data) | ||||||
|  | 
 | ||||||
| @app.route('/') | @app.route('/') | ||||||
| async def root() -> Response: | async def root() -> Response: | ||||||
|     return await app.send_static_file('index.html') |     return await app.send_static_file('index.html') | ||||||
| 
 | 
 | ||||||
| # Every time a websocket connection to /glebby is opened, a new connection |  | ||||||
| # is set up in the connection manager. It will handle sending and receiving |  | ||||||
| # and keeping the model up to date. |  | ||||||
| @app.websocket('/glebby') | @app.websocket('/glebby') | ||||||
| async def ws() -> NoReturn: | async def ws() -> None: | ||||||
|     await connection_manager.setup_connection() |     global model | ||||||
|  | 
 | ||||||
|  |     # For each connected client, we create a queue to store its outgoing messages. | ||||||
|  |     # We pass this queue to the model so that it can communicate with the clients. | ||||||
|  |     outgoing_queue: asyncio.Queue[str] = asyncio.Queue() | ||||||
|  |     client_id = await model.add_client(outgoing_queue) | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         # In order to get the queued messages through the websocket, we need to create a task | ||||||
|  |         # since the current one will be busy receiving incoming messages below.   | ||||||
|  |         outgoing_task = asyncio.create_task(send_outgoing(outgoing_queue)) | ||||||
|  | 
 | ||||||
|  |         while True: | ||||||
|  |             data = await websocket.receive() | ||||||
|  |             await model.handle_incoming(client_id, data) | ||||||
|  |     # When a client disconnects, we shut down the message pumping task and wait for it to finish | ||||||
|  |     except asyncio.CancelledError: | ||||||
|  |         await model.remove_client(client_id) | ||||||
|  |         outgoing_task.cancel() | ||||||
|  |         await outgoing_task | ||||||
|  | |||||||
| @ -1,73 +0,0 @@ | |||||||
| import asyncio |  | ||||||
| import itertools |  | ||||||
| 
 |  | ||||||
| from collections import OrderedDict |  | ||||||
| from quart import websocket |  | ||||||
| from typing import Any, AsyncGenerator, Awaitable, Callable, Iterator, NoReturn, Optional |  | ||||||
| 
 |  | ||||||
| # Represents a single websocket connection |  | ||||||
| class Connection: |  | ||||||
|     outgoing_queue: asyncio.Queue[str] |  | ||||||
| 
 |  | ||||||
|     def __init__(self) -> None: |  | ||||||
|         self.outgoing_queue = asyncio.Queue() |  | ||||||
|      |  | ||||||
|     # Sending a message simply puts it in the outgoing queue... |  | ||||||
|     async def send(self, data: str) -> None: |  | ||||||
|         await self.outgoing_queue.put(data) |  | ||||||
|      |  | ||||||
|     # ... and this infinite loop takes care of actually sending the outgoing |  | ||||||
|     # messages through the websocket connection. |  | ||||||
|     async def _send_loop(self) -> None: |  | ||||||
|         while True: |  | ||||||
|             data = await self.outgoing_queue.get() |  | ||||||
|             await websocket.send(data) |  | ||||||
| 
 |  | ||||||
| class ConnectionManager: |  | ||||||
|     # Provides a *unique* identifier for each connection |  | ||||||
|     id_generator: Iterator[int] |  | ||||||
|     connections: OrderedDict[int, Connection] |  | ||||||
| 
 |  | ||||||
|     on_open: Optional[Callable[[int], Awaitable[Any]]] |  | ||||||
|     on_message: Optional[Callable[[int, str], Awaitable[Any]]] |  | ||||||
|     on_close: Optional[Callable[[int], Awaitable[Any]]] |  | ||||||
| 
 |  | ||||||
|     def __init__(self) -> None: |  | ||||||
|         self.id_generator = itertools.count(start=0, step=1) |  | ||||||
|         self.connections = OrderedDict() |  | ||||||
| 
 |  | ||||||
|         self.on_open = None |  | ||||||
|         self.on_message = None |  | ||||||
|         self.on_close = None |  | ||||||
| 
 |  | ||||||
|     # Wires up asyncio tasks etc. such that on_open, on_message and on_close |  | ||||||
|     # are called at the right time with the right arguments. |  | ||||||
|     async def setup_connection(self) -> NoReturn: |  | ||||||
|         connection_id = next(self.id_generator) |  | ||||||
|         connection = Connection() |  | ||||||
|         self.connections[connection_id] = connection |  | ||||||
| 
 |  | ||||||
|         if self.on_open: |  | ||||||
|             await self.on_open(connection_id) |  | ||||||
| 
 |  | ||||||
|         try: |  | ||||||
|             send_task = asyncio.create_task(connection._send_loop()) |  | ||||||
|             while True: |  | ||||||
|                 data = await websocket.receive() |  | ||||||
|                 if self.on_message: |  | ||||||
|                     await self.on_message(connection_id, data) |  | ||||||
| 
 |  | ||||||
|         except asyncio.CancelledError: |  | ||||||
|             del self.connections[connection_id] |  | ||||||
|             send_task.cancel() |  | ||||||
|             await send_task |  | ||||||
| 
 |  | ||||||
|             if self.on_close: |  | ||||||
|                 await self.on_close(connection_id) |  | ||||||
|              |  | ||||||
|             raise |  | ||||||
|      |  | ||||||
|     # Interface for the model to send messages. The model shouldn't |  | ||||||
|     # use the Connection class at all. |  | ||||||
|     async def send_to(self, connection_id: int, data: str) -> None: |  | ||||||
|         await self.connections[connection_id].send(data) |  | ||||||
| @ -1,60 +1,62 @@ | |||||||
|  | import asyncio | ||||||
|  | import itertools | ||||||
| import json | import json | ||||||
| 
 | 
 | ||||||
| from collections import OrderedDict | from collections import OrderedDict | ||||||
| from typing import Any, Iterator, Optional | from typing import Any, Iterator, Optional | ||||||
| 
 | 
 | ||||||
| from glebby.board import BoardGenerator | from glebby.board import BoardGenerator | ||||||
| from glebby.connection_manager import ConnectionManager |  | ||||||
| 
 | 
 | ||||||
| class Client: | class Client: | ||||||
|  |     outgoing_queue: asyncio.Queue[str] | ||||||
|     id: int |     id: int | ||||||
|     name: str |     name: str | ||||||
| 
 | 
 | ||||||
|     def __init__(self, client_id: int): |     def __init__(self, client_id: int, outgoing_queue: asyncio.Queue[str]): | ||||||
|  |         self.outgoing_queue = outgoing_queue | ||||||
|         self.id = client_id |         self.id = client_id | ||||||
|         self.name = '' |         self.name = '' | ||||||
|      |      | ||||||
|  |     async def put_outgoing_message(self, message: str) -> None: | ||||||
|  |         await self.outgoing_queue.put(message) | ||||||
|  |      | ||||||
|     def get_dto(self) -> dict[str, Any]: |     def get_dto(self) -> dict[str, Any]: | ||||||
|         return { 'id': self.id, 'name': self.name } |         return { 'id': self.id, 'name': self.id } | ||||||
| 
 | 
 | ||||||
| class Model: | class Model: | ||||||
|     connection_manager: ConnectionManager |  | ||||||
|     clients: OrderedDict[int, Client] |     clients: OrderedDict[int, Client] | ||||||
|  |     id_generator: Iterator[int] | ||||||
|     board_generator: BoardGenerator |     board_generator: BoardGenerator | ||||||
|     board: Optional[list[list[str]]] |     board: Optional[list[list[str]]] | ||||||
| 
 | 
 | ||||||
|     def __init__(self, connection_manager: ConnectionManager): |     def __init__(self) -> None: | ||||||
|         self.connection_manager = connection_manager |  | ||||||
|         self.clients = OrderedDict() |         self.clients = OrderedDict() | ||||||
|  |         self.id_generator = itertools.count(start=0, step=1) | ||||||
|         self.board_generator = BoardGenerator(None) |         self.board_generator = BoardGenerator(None) | ||||||
|         self.board = None |         self.board = None | ||||||
|      |      | ||||||
|         self.connection_manager.on_open = self.add_client |     async def add_client(self, outgoing_queue: asyncio.Queue[str]) -> int: | ||||||
|         self.connection_manager.on_message = self.handle_incoming |         client_id = next(self.id_generator) | ||||||
|         self.connection_manager.on_close = self.remove_client |         await self.broadcast(client_id, { 'type': 'join' }) | ||||||
| 
 | 
 | ||||||
|     async def add_client(self, client_id: int) -> None: |         self.clients[client_id] = Client(client_id, outgoing_queue) | ||||||
|         print(f'<{client_id}|OPEN>') |         print(f'<{client_id}|join>') | ||||||
| 
 | 
 | ||||||
|         await self.broadcast(client_id, { |  | ||||||
|             'type': 'join' |  | ||||||
|         }) |  | ||||||
|         self.clients[client_id] = Client(client_id) |  | ||||||
|         await self.send_to(None, client_id, { |         await self.send_to(None, client_id, { | ||||||
|             'type': 'init', |             'type': 'init', | ||||||
|             'state': self.get_state_dto(client_id) |             'state': self.get_state_dto(client_id) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|     async def remove_client(self, client_id: int) -> None: |         return client_id | ||||||
|         print(f'<{client_id}|CLOS>') |  | ||||||
|      |      | ||||||
|  |     async def remove_client(self, client_id: int) -> None: | ||||||
|         del self.clients[client_id] |         del self.clients[client_id] | ||||||
|         await self.broadcast(client_id, { |         await self.broadcast(client_id, { 'type': 'leave' }) | ||||||
|             'type': 'leave' |          | ||||||
|         }) |         print(f'<{client_id}|leave>') | ||||||
|      |      | ||||||
|     async def send_to(self, client_id_from: Optional[int], client_id_to: int, payload: Any) -> None: |     async def send_to(self, client_id_from: Optional[int], client_id_to: int, payload: Any) -> None: | ||||||
|         await self.connection_manager.send_to(client_id_to, json.dumps({ |         await self.clients[client_id_to].put_outgoing_message(json.dumps({ | ||||||
|             'from': client_id_from, |             'from': client_id_from, | ||||||
|             'payload': payload |             'payload': payload | ||||||
|         })) |         })) | ||||||
| @ -63,24 +65,9 @@ class Model: | |||||||
|         for client_id_to in self.clients: |         for client_id_to in self.clients: | ||||||
|             await self.send_to(client_id_from, client_id_to, payload) |             await self.send_to(client_id_from, client_id_to, payload) | ||||||
|      |      | ||||||
|     async def handle_incoming(self, client_id: int, data: str) -> None: |     async def handle_incoming(self, client_id: int, message: str) -> None: | ||||||
|         print(f'<{client_id}|DATA> {data}') |         print(f'<{client_id}|data> {message}') | ||||||
| 
 |         payload = json.loads(message) | ||||||
|         try: |  | ||||||
|             payload = json.loads(data) |  | ||||||
|         except: |  | ||||||
|             await self.send_to(None, client_id, { |  | ||||||
|                 'type': 'error', |  | ||||||
|                 'errorMessage': 'you sent invalid JSON!' |  | ||||||
|             }) |  | ||||||
|             return |  | ||||||
|          |  | ||||||
|         if 'type' not in payload: |  | ||||||
|             await self.send_to(None, client_id, { |  | ||||||
|                 'type': 'error', |  | ||||||
|                 'errorMessage': 'missing message type!' |  | ||||||
|             }) |  | ||||||
|             return |  | ||||||
|          |          | ||||||
|         if payload['type'] == 'set-name': |         if payload['type'] == 'set-name': | ||||||
|             self.clients[client_id].name = payload['name'] |             self.clients[client_id].name = payload['name'] | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user