diff --git a/examples/chat-room-python/.gitignore b/examples/chat-room-python/.gitignore new file mode 100644 index 000000000..79b7a1192 --- /dev/null +++ b/examples/chat-room-python/.gitignore @@ -0,0 +1,2 @@ +.actorcore +node_modules \ No newline at end of file diff --git a/examples/chat-room-python/actors/app.ts b/examples/chat-room-python/actors/app.ts new file mode 100644 index 000000000..21e9577b5 --- /dev/null +++ b/examples/chat-room-python/actors/app.ts @@ -0,0 +1,35 @@ +import { actor, setup } from "actor-core"; + +// state managed by the actor +export interface State { + messages: { username: string; message: string }[]; +} + +export const chatRoom = actor({ + // initialize state + state: { messages: [] } as State, + + // define actions + actions: { + // receive an action call from the client + sendMessage: (c, username: string, message: string) => { + // save message to persistent storage + c.state.messages.push({ username, message }); + + // broadcast message to all clients + c.broadcast("newMessage", username, message); + }, + + getHistory: (c) => { + return c.state.messages; + }, + }, +}); + +// Create and export the app +export const app = setup({ + actors: { chatRoom }, +}); + +// Export type for client type checking +export type App = typeof app; diff --git a/examples/chat-room-python/package.json b/examples/chat-room-python/package.json new file mode 100644 index 000000000..58881e7ec --- /dev/null +++ b/examples/chat-room-python/package.json @@ -0,0 +1,23 @@ +{ + "name": "chat-room-python", + "version": "0.8.0", + "private": true, + "type": "module", + "scripts": { + "dev": "npx @actor-core/cli@latest dev actors/app.ts", + "check-types": "tsc --noEmit", + "pytest": "pytest tests/test_chat_room.py -v" + }, + "devDependencies": { + "@actor-core/cli": "workspace:*", + "@types/node": "^22.13.9", + "actor-core": "workspace:*", + "tsx": "^3.12.7", + "typescript": "^5.5.2" + }, + "example": { + "platforms": [ + "*" + ] + } +} diff --git a/examples/chat-room-python/requirements.txt b/examples/chat-room-python/requirements.txt new file mode 100644 index 000000000..7a28c733d --- /dev/null +++ b/examples/chat-room-python/requirements.txt @@ -0,0 +1,4 @@ +actor-core-client>=0.8.0 +prompt_toolkit>=3.0.0 +pytest>=7.0.0 +pytest-asyncio>=0.21.0 \ No newline at end of file diff --git a/examples/chat-room-python/scripts/cli.py b/examples/chat-room-python/scripts/cli.py new file mode 100644 index 000000000..1269e1256 --- /dev/null +++ b/examples/chat-room-python/scripts/cli.py @@ -0,0 +1,52 @@ +import asyncio +from actor_core_client import AsyncClient as ActorClient +import prompt_toolkit +from prompt_toolkit.patch_stdout import patch_stdout +from typing import TypedDict, List + +async def init_prompt() -> tuple[str, str]: + username = await prompt_toolkit.prompt_async("Username: ") + room = await prompt_toolkit.prompt_async("Room: ") + return username, room + +async def main(): + # Get username and room + username, room = await init_prompt() + print(f"Joining room '{room}' as '{username}'") + + # Create client and connect to chat room + client = ActorClient("http://localhost:6420") + chat_room = await client.get("chatRoom", tags={"room": room}, params={"room": room}) + + # Get and display history + history = await chat_room.action("getHistory", []) + if history: + print("\nHistory:") + for msg in history: + print(f"[{msg['username']}] {msg['message']}") + + # Set up message handler + def on_message(username: str, message: str): + print(f"\n[{username}] {message}") + + chat_room.on_event("newMessage", on_message) + + # Main message loop + print("\nStart typing messages (press Ctrl+D or send empty message to exit)") + try: + with patch_stdout(): + while True: + # NOTE: Using prompt_toolkit to keep messages + # intact, regardless of other threads / tasks. + message = await prompt_toolkit.prompt_async("\nMessage: ") + if not message: + break + await chat_room.action("sendMessage", [username, message]) + except EOFError: + pass + finally: + print("\nDisconnecting...") + await chat_room.disconnect() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/chat-room-python/scripts/connect.py b/examples/chat-room-python/scripts/connect.py new file mode 100644 index 000000000..ba2482d65 --- /dev/null +++ b/examples/chat-room-python/scripts/connect.py @@ -0,0 +1,30 @@ +import asyncio +import os +from actor_core_client import AsyncClient as ActorClient + +async def main(): + # Create client + endpoint = os.getenv("ENDPOINT", "http://localhost:6420") + client = ActorClient(endpoint) + + # Connect to chat room + chat_room = await client.get("chatRoom") + + # Get existing messages + messages = await chat_room.action("getHistory", []) + print("Messages:", messages) + + # Listen for new messages + def on_message(username: str, message: str): + print(f"Message from {username}: {message}") + + chat_room.on_event("newMessage", on_message) + + # Send message to room + await chat_room.action("sendMessage", ["william", "All the world's a stage."]) + + # Disconnect from actor when finished + await chat_room.disconnect() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/chat-room-python/tests/test_chat_room.py b/examples/chat-room-python/tests/test_chat_room.py new file mode 100644 index 000000000..9af59b295 --- /dev/null +++ b/examples/chat-room-python/tests/test_chat_room.py @@ -0,0 +1,66 @@ +import pytest +from actor_core_client import AsyncClient as ActorClient +from actor_core_test import setup_test +from typing import TypedDict, List + + +async def test_chat_room_should_handle_messages(): + # Set up test environment + client = await setup_test() + + # Connect to chat room + chat_room = await client.get("chatRoom") + + # Initial history should be empty + initial_messages = await chat_room.action("getHistory", []) + assert initial_messages == [] + + # Test event emission + received_data = {"username": "", "message": ""} + + def on_message(username: str, message: str): + received_data["username"] = username + received_data["message"] = message + + chat_room.on_event("newMessage", on_message) + + # Send a message + test_user = "william" + test_message = "All the world's a stage." + await chat_room.action("sendMessage", [test_user, test_message]) + + # Verify event was emitted with correct data + assert received_data["username"] == test_user + assert received_data["message"] == test_message + + # Verify message was stored in history + updated_messages = await chat_room.action("getHistory", []) + assert updated_messages == [{"username": test_user, "message": test_message}] + + # Send multiple messages and verify + users = ["romeo", "juliet", "othello"] + messages = [ + "Wherefore art thou?", + "Here I am!", + "The green-eyed monster." + ] + + for i in range(len(users)): + await chat_room.action("sendMessage", [users[i], messages[i]]) + + # Verify event emission + assert received_data["username"] == users[i] + assert received_data["message"] == messages[i] + + # Verify all messages are in history in correct order + final_history = await chat_room.action("getHistory", []) + expected_history = [{"username": test_user, "message": test_message}] + expected_history.extend([ + {"username": users[i], "message": messages[i]} + for i in range(len(users)) + ]) + + assert final_history == expected_history + + # Cleanup + await chat_room.disconnect() \ No newline at end of file diff --git a/examples/chat-room-python/tsconfig.json b/examples/chat-room-python/tsconfig.json new file mode 100644 index 000000000..474d2882c --- /dev/null +++ b/examples/chat-room-python/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "esnext", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["esnext"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "esnext", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "bundler", + /* Specify type package names to be included without being referenced in a source file. */ + "types": ["node"], + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Disable emitting files from a compilation. */ + "noEmit": true, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true + }, + "include": ["src/**/*", "actors/**/*", "tests/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 59e66df54..b756d1f33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3897,6 +3897,18 @@ __metadata: languageName: node linkType: hard +"chat-room-python@workspace:examples/chat-room-python": + version: 0.0.0-use.local + resolution: "chat-room-python@workspace:examples/chat-room-python" + dependencies: + "@actor-core/cli": "workspace:*" + "@types/node": "npm:^22.13.9" + actor-core: "workspace:*" + tsx: "npm:^3.12.7" + typescript: "npm:^5.5.2" + languageName: unknown + linkType: soft + "chat-room@workspace:examples/chat-room": version: 0.0.0-use.local resolution: "chat-room@workspace:examples/chat-room"