Describe the bug
The websocket limits only one coroutine to await on recv to prevent racing problem. However current pyvts introduces blocking behavior in
Because every spawned coroutine will await on the same websocket object and cause error.
To Reproduce
Steps to reproduce the behavior:
Send multiple requests at once using asyncio.gather.
RuntimeError: cannot call recv while another coroutine is already waiting for the next message
Expected behavior
Requests should be fully async no matter how many requests are being sent at once.
Desktop (please complete the following information):
OS: Windows
Version: 0.3.3
Additional context
I have a draft implementation of a full async WS event handler here (though it's very not following the original implementation of vts):
class vts:
"""
A fully async implementation of VTube Studio API
"""
ws: WebSocketClientProtocol
requests: dict[str, Event]
def __init__(
self,
name: str,
developer: str,
icon: str = None,
auth_token: str = None,
ip: str = "localhost",
port: int = 8001,
) -> None:
self.auth_info = {
"pluginName": name,
"pluginDeveloper": developer,
"pluginIcon": icon,
}
self.auth_token = auth_token
self.ip = ip
self.port = port
self.ws = None
self.requests = {}
async def connect(self) -> None:
if self.ws is not None:
raise RuntimeError("Already connected")
self.ws = await websockets.connect(f"ws://{self.ip}:{self.port}")
self.recv_loop = asyncio.create_task(self.run_recv_loop())
async def run_recv_loop(self) -> None:
"""
Only the recv loop should read from the websocket.
We identify each incoming response by the requestID.
"""
async for message in self.ws:
resp = json.loads(message)
id = resp["requestID"]
event = self.requests.pop(id)
if event is not None:
setattr(event, "data", resp.get("data", None))
event.set() # Notify the request is done
async def request(
self,
command: str,
payload: dict[str, Any] = None,
id: str = None,
) -> None:
"""
Send a request to the server. This does not throw if there are
multiple requests.
"""
if id is None:
id = str(uuid4())
apiPayload = {
"apiName": "VTubeStudioPublicAPI",
"apiVersion": "1.0",
"requestID": id,
"messageType": command,
}
if payload is not None:
apiPayload["payload"] = payload
event = Event() # We make a signal here to wait for the response
self.requests[id] = event
await self.ws.send(json.dumps(apiPayload))
await event.wait()
return getattr(event, "data", None)
async def close(self) -> None:
self.recv_loop.cancel()
await self.ws.close()
self.ws = None
self.requests = {}
Yeah I found the problem before, but I don't know how to resolve it then... Thanks for your suggestion and I would definitely check out your implementation.
Describe the bug The websocket limits only one coroutine to await on
recv
to prevent racing problem. However current pyvts introduces blocking behavior inhttps://github.com/Genteki/pyvts/blob/3c5fb840fe7e962830d0633d1e095f3409baab35/pyvts/vts.py#L117-L118
Because every spawned coroutine will await on the same websocket object and cause error.
To Reproduce Steps to reproduce the behavior:
asyncio.gather
.RuntimeError: cannot call recv while another coroutine is already waiting for the next message
Expected behavior Requests should be fully async no matter how many requests are being sent at once.
Desktop (please complete the following information):
Additional context
I have a draft implementation of a full async WS event handler here (though it's very not following the original implementation of vts):