Use Socket.IO for message transport (#449)
This commit is contained in:
parent
8958f14778
commit
50a7c02142
648
poetry.lock
generated
648
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -25,11 +25,11 @@
|
|||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-plotly.js": "^2.6.0",
|
"react-plotly.js": "^2.6.0",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"reconnecting-websocket": "^4.4.0",
|
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
|
"socket.io-client": "^4.5.4",
|
||||||
"victory": "^36.6.8"
|
"victory": "^36.6.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
0.1.14
|
0.1.15
|
@ -1,5 +1,5 @@
|
|||||||
// State management for Pynecone web apps.
|
// State management for Pynecone web apps.
|
||||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
import io from 'socket.io-client';
|
||||||
|
|
||||||
// Global variable to hold the token.
|
// Global variable to hold the token.
|
||||||
let token;
|
let token;
|
||||||
@ -90,7 +90,7 @@ export const applyEvent = async (event, router, socket) => {
|
|||||||
event.token = getToken();
|
event.token = getToken();
|
||||||
event.router_data = (({ pathname, query }) => ({ pathname, query }))(router);
|
event.router_data = (({ pathname, query }) => ({ pathname, query }))(router);
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.send(JSON.stringify(event));
|
socket.emit("event", JSON.stringify(event));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,11 +109,6 @@ export const updateState = async (state, setState, result, setResult, router, so
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the socket is not ready, return.
|
|
||||||
if (!socket.readyState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set processing to true to block other events from being processed.
|
// Set processing to true to block other events from being processed.
|
||||||
setResult({ ...result, processing: true });
|
setResult({ ...result, processing: true });
|
||||||
|
|
||||||
@ -137,23 +132,25 @@ export const updateState = async (state, setState, result, setResult, router, so
|
|||||||
*/
|
*/
|
||||||
export const connect = async (socket, state, setState, result, setResult, router, endpoint) => {
|
export const connect = async (socket, state, setState, result, setResult, router, endpoint) => {
|
||||||
// Create the socket.
|
// Create the socket.
|
||||||
socket.current = new ReconnectingWebSocket(endpoint);
|
socket.current = io(endpoint, {
|
||||||
|
'path': '/event',
|
||||||
|
});
|
||||||
|
|
||||||
// Once the socket is open, hydrate the page.
|
// Once the socket is open, hydrate the page.
|
||||||
socket.current.onopen = () => {
|
socket.current.on('connect', () => {
|
||||||
updateState(state, setState, result, setResult, router, socket.current)
|
updateState(state, setState, result, setResult, router, socket.current);
|
||||||
}
|
});
|
||||||
|
|
||||||
// On each received message, apply the delta and set the result.
|
// On each received message, apply the delta and set the result.
|
||||||
socket.current.onmessage = function (update) {
|
socket.current.on('event', function (update) {
|
||||||
update = JSON.parse(update.data);
|
update = JSON.parse(update);
|
||||||
applyDelta(state, update.delta);
|
applyDelta(state, update.delta);
|
||||||
setResult({
|
setResult({
|
||||||
processing: false,
|
processing: false,
|
||||||
state: state,
|
state: state,
|
||||||
events: update.events,
|
events: update.events,
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
140
pynecone/app.py
140
pynecone/app.py
@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Union
|
from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Union
|
||||||
|
|
||||||
from fastapi import FastAPI, WebSocket
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware import cors
|
from socketio import ASGIApp, AsyncNamespace, AsyncServer
|
||||||
from starlette.websockets import WebSocketDisconnect
|
|
||||||
|
|
||||||
from pynecone import constants, utils
|
from pynecone import constants, utils
|
||||||
from pynecone.base import Base
|
from pynecone.base import Base
|
||||||
@ -33,6 +32,9 @@ class App(Base):
|
|||||||
# The backend API object.
|
# The backend API object.
|
||||||
api: FastAPI = None # type: ignore
|
api: FastAPI = None # type: ignore
|
||||||
|
|
||||||
|
# The Socket.IO AsyncServer.
|
||||||
|
sio: AsyncServer = None
|
||||||
|
|
||||||
# The state class to use for the app.
|
# The state class to use for the app.
|
||||||
state: Type[State] = DefaultState
|
state: Type[State] = DefaultState
|
||||||
|
|
||||||
@ -64,10 +66,23 @@ class App(Base):
|
|||||||
self.state_manager.setup(state=self.state)
|
self.state_manager.setup(state=self.state)
|
||||||
|
|
||||||
# Set up the API.
|
# Set up the API.
|
||||||
|
|
||||||
self.api = FastAPI()
|
self.api = FastAPI()
|
||||||
self.add_cors()
|
|
||||||
self.add_default_endpoints()
|
# Set up the Socket.IO AsyncServer.
|
||||||
|
self.sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*")
|
||||||
|
|
||||||
|
# Create the socket app. Note event endpoint constant replaces the default 'socket.io' path.
|
||||||
|
socket_app = ASGIApp(self.sio, socketio_path=str(constants.Endpoint.EVENT))
|
||||||
|
|
||||||
|
# Create the event namespace and attach the main app. Not related to the path above.
|
||||||
|
event_namespace = EventNamespace("/event")
|
||||||
|
event_namespace.app = self
|
||||||
|
|
||||||
|
# Register the event namespace with the socket.
|
||||||
|
self.sio.register_namespace(event_namespace)
|
||||||
|
|
||||||
|
# Mount the socket app with the API.
|
||||||
|
self.api.mount("/", socket_app)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Get the string representation of the app.
|
"""Get the string representation of the app.
|
||||||
@ -85,24 +100,6 @@ class App(Base):
|
|||||||
"""
|
"""
|
||||||
return self.api
|
return self.api
|
||||||
|
|
||||||
def add_default_endpoints(self):
|
|
||||||
"""Add the default endpoints."""
|
|
||||||
# To test the server.
|
|
||||||
self.api.get(str(constants.Endpoint.PING))(ping)
|
|
||||||
|
|
||||||
# To make state changes.
|
|
||||||
self.api.websocket(str(constants.Endpoint.EVENT))(event(app=self))
|
|
||||||
|
|
||||||
def add_cors(self):
|
|
||||||
"""Add CORS middleware to the app."""
|
|
||||||
self.api.add_middleware(
|
|
||||||
cors.CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def preprocess(self, state: State, event: Event) -> Optional[Delta]:
|
def preprocess(self, state: State, event: Event) -> Optional[Delta]:
|
||||||
"""Preprocess the event.
|
"""Preprocess the event.
|
||||||
|
|
||||||
@ -327,52 +324,6 @@ class App(Base):
|
|||||||
compiler.compile_components(custom_components)
|
compiler.compile_components(custom_components)
|
||||||
|
|
||||||
|
|
||||||
async def ping() -> str:
|
|
||||||
"""Test API endpoint.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The response.
|
|
||||||
"""
|
|
||||||
return "pong"
|
|
||||||
|
|
||||||
|
|
||||||
def event(app: App):
|
|
||||||
"""Websocket endpoint for events.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app: The app to add the endpoint to.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The websocket endpoint.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def ws(websocket: WebSocket):
|
|
||||||
"""Create websocket endpoint.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
websocket: The websocket sending events.
|
|
||||||
"""
|
|
||||||
# Accept the connection.
|
|
||||||
await websocket.accept()
|
|
||||||
|
|
||||||
# Process events until the connection is closed.
|
|
||||||
while True:
|
|
||||||
# Get the event.
|
|
||||||
try:
|
|
||||||
event = Event.parse_raw(await websocket.receive_text())
|
|
||||||
except WebSocketDisconnect:
|
|
||||||
# Close the connection.
|
|
||||||
return
|
|
||||||
|
|
||||||
# Process the event.
|
|
||||||
update = await process(app, event)
|
|
||||||
|
|
||||||
# Send the update.
|
|
||||||
await websocket.send_text(update.json())
|
|
||||||
|
|
||||||
return ws
|
|
||||||
|
|
||||||
|
|
||||||
async def process(app: App, event: Event) -> StateUpdate:
|
async def process(app: App, event: Event) -> StateUpdate:
|
||||||
"""Process an event.
|
"""Process an event.
|
||||||
|
|
||||||
@ -405,3 +356,52 @@ async def process(app: App, event: Event) -> StateUpdate:
|
|||||||
|
|
||||||
# Return the update.
|
# Return the update.
|
||||||
return update
|
return update
|
||||||
|
|
||||||
|
|
||||||
|
class EventNamespace(AsyncNamespace):
|
||||||
|
"""The event namespace."""
|
||||||
|
|
||||||
|
# The backend API object.
|
||||||
|
app: App
|
||||||
|
|
||||||
|
def on_connect(self, sid, environ):
|
||||||
|
"""Event for when the websocket disconnects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sid: The Socket.IO session id.
|
||||||
|
environ: The request information, including HTTP headers.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_disconnect(self, sid):
|
||||||
|
"""Event for when the websocket disconnects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sid: The Socket.IO session id.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_event(self, sid, data):
|
||||||
|
"""Event for receiving front-end websocket events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sid: The Socket.IO session id.
|
||||||
|
data: The event data.
|
||||||
|
"""
|
||||||
|
# Get the event.
|
||||||
|
event = Event.parse_raw(data)
|
||||||
|
|
||||||
|
# Process the event.
|
||||||
|
update = await process(self.app, event)
|
||||||
|
|
||||||
|
# Emit the event.
|
||||||
|
await self.emit(str(constants.SocketEvent.EVENT), update.json(), to=sid)
|
||||||
|
|
||||||
|
async def on_ping(self, sid):
|
||||||
|
"""Event for testing the API endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sid: The Socket.IO session id.
|
||||||
|
"""
|
||||||
|
# Emit the test event.
|
||||||
|
await self.emit(str(constants.SocketEvent.PING), "pong", to=sid)
|
||||||
|
@ -164,14 +164,8 @@ USE_EFFECT = join(
|
|||||||
" if(!isReady) {{",
|
" if(!isReady) {{",
|
||||||
" return;",
|
" return;",
|
||||||
" }}",
|
" }}",
|
||||||
" const reconnectSocket = () => {{",
|
f" if (!{SOCKET}.current) {{{{",
|
||||||
f" {SOCKET}.current.reconnect()",
|
f" connect({SOCKET}, {{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {EVENT_ENDPOINT})",
|
||||||
" }}",
|
|
||||||
f" if (typeof {SOCKET}.current !== 'undefined') {{{{",
|
|
||||||
f" if (!{SOCKET}.current) {{{{",
|
|
||||||
f" window.addEventListener('focus', reconnectSocket)",
|
|
||||||
f" connect({SOCKET}, {{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {EVENT_ENDPOINT})",
|
|
||||||
" }}",
|
|
||||||
" }}",
|
" }}",
|
||||||
" const update = async () => {{",
|
" const update = async () => {{",
|
||||||
f" if ({RESULT}.{STATE} != null) {{{{",
|
f" if ({RESULT}.{STATE} != null) {{{{",
|
||||||
|
@ -159,7 +159,6 @@ class LogLevel(str, Enum):
|
|||||||
class Endpoint(Enum):
|
class Endpoint(Enum):
|
||||||
"""Endpoints for the pynecone backend API."""
|
"""Endpoints for the pynecone backend API."""
|
||||||
|
|
||||||
PING = "ping"
|
|
||||||
EVENT = "event"
|
EVENT = "event"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -192,6 +191,21 @@ class Endpoint(Enum):
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
class SocketEvent(Enum):
|
||||||
|
"""Socket events sent by the pynecone backend API."""
|
||||||
|
|
||||||
|
PING = "ping"
|
||||||
|
EVENT = "event"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Get the string representation of the event name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The event name string.
|
||||||
|
"""
|
||||||
|
return str(self.value)
|
||||||
|
|
||||||
|
|
||||||
class RouteArgType(SimpleNamespace):
|
class RouteArgType(SimpleNamespace):
|
||||||
"""Type of dynamic route arg extracted from URI route."""
|
"""Type of dynamic route arg extracted from URI route."""
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ uvicorn = "^0.20.0"
|
|||||||
rich = "^12.6.0"
|
rich = "^12.6.0"
|
||||||
redis = "^4.3.5"
|
redis = "^4.3.5"
|
||||||
httpx = "^0.23.1"
|
httpx = "^0.23.1"
|
||||||
websockets = "^10.4"
|
python-socketio = "^5.7.2"
|
||||||
psutil = "^5.9.4"
|
psutil = "^5.9.4"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
Loading…
Reference in New Issue
Block a user