Coverage for src/aiocoap/util/pyodide_websockets.py: 0%
59 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-12 11:18 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-12 11:18 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""This module provides a slimmed-down replacement of the websockets_ module
6(that regularly powers :mod:`aiocoap.transports.ws`) -- but implemented through
7pyodide_'s JavaScript adapter towards the WebSocket module of the hosting
8browser. It aims to be a drop-in replacement that provides the parts that can
9be implemented in the browser, and to provide practical errors on the used
10entry points. It will not go out of its way to mimick every aspect of the
11websockets module, but restrain itself to what ``.ws`` needs.
13**Future developement:** The module can probably be extended to cover all the
14implementable functionality of websockets, and provide meaningful errors on all
15its items. When that happens, it should be split out of aiocoap.
17.. _websockets: https://websockets.readthedocs.io/
18.. _pyodide: https://pyodide.org/
19"""
21import asyncio
24class WebSocketCommonProtocol:
25 pass
28class WebSocketClientProtocol(WebSocketCommonProtocol):
29 def __init__(self, socket):
30 # Note that this is an under-implemented constructor -- the real thing
31 # is in `connect()` which is async enough to do more.
33 self._socket = socket
34 # FIXME: This is a workaround for WebSockets' shortcomings, while
35 # WebSocketStreams are not deployed (see
36 # https://developer.chrome.com/articles/websocketstream/ for details)
38 self._queue = asyncio.Queue()
40 # The initial setting doesn't matter too much because we're not handing
41 # it out before setting this to True ... still feels cleaner this way.
42 self.open = False
44 async def recv(self):
45 (etype, event) = await self._queue.get()
46 if etype == "message":
47 if isinstance(event.data, str):
48 # FIXME: Test this
49 return event.data
50 return bytes((await event.data.arrayBuffer()).to_py())
51 elif etype == "close":
52 raise exceptions.ConnectionClosed()
53 elif etype == "error":
54 raise exceptions.WebSocketException("Connection error")
55 else:
56 raise RuntimeError("Unknown event in queue")
58 async def send(self, msg):
59 from js import Blob, Uint8Array
61 blob = Blob.new([Uint8Array.new(msg)])
62 self._socket.send(blob)
64 # FIXME: It'd be preferable if we could make this an unassigned property
65 # that'd raise if anybody tried to access it (for neither do we know the
66 # value, nor could anything useful be done with it), but as things are,
67 # we'll have to rely on all users' sensibilities to not send around
68 # addresses that are not globally usable. (The port, indicating the default
69 # port, is an outright lie, though.)
70 local_address = ("localhost", None)
72 def on_message(self, event):
73 self._queue.put_nowait(("message", event))
75 def on_error(self, event):
76 self.open = False
77 self._queue.put_nowait(("error", event))
79 def on_close(self, event):
80 self.open = False
81 self._queue.put_nowait(("close", event))
84async def connect(
85 uri, subprotocols=None, ping_interval=20, ssl=None
86) -> WebSocketClientProtocol:
87 from pyodide.ffi.wrappers import add_event_listener
88 from js import WebSocket
90 if ssl is not None:
91 raise ValueError("SSL can not be configured within the browser WebSocket API")
93 socket = WebSocket.new(uri, subprotocols)
95 # Ignoring ping_interval: We can't tell what the browser does, and it may
96 # be right nor not.
98 proto = WebSocketClientProtocol(socket)
100 add_event_listener(
101 socket, "open", lambda e, q=proto._queue: q.put_nowait(("open", e))
102 )
103 add_event_listener(socket, "message", proto.on_message)
104 add_event_listener(socket, "error", proto.on_error)
105 add_event_listener(socket, "close", proto.on_close)
106 (etype, event) = await proto._queue.get()
107 if etype != "open":
108 raise exceptions.WebSocketException("Failed to connect")
109 proto.open = True
111 return proto
114class exceptions:
115 """A class that is a good-enough approximation of ``websockets.exceptions``
116 to get away with a single file implementing pyodide_websockets."""
118 class WebSocketException(Exception):
119 pass
121 class ConnectionClosed(WebSocketException):
122 pass
125# Mocks required by the aiocoap.transports.ws module expecting a full implementation
128class WebSocketServerProtocol:
129 def __init__(self, *args, **kwargs):
130 raise RuntimeError("Web sockets in web browsers can not be used as servers")
133WebSocketServer = WebSocketServerProtocol
136def serve(*args, **kwargs):
137 WebSocketServer()