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

1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors 

2# 

3# SPDX-License-Identifier: MIT 

4 

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. 

12 

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. 

16 

17.. _websockets: https://websockets.readthedocs.io/ 

18.. _pyodide: https://pyodide.org/ 

19""" 

20 

21import asyncio 

22 

23 

24class WebSocketCommonProtocol: 

25 pass 

26 

27 

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. 

32 

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) 

37 

38 self._queue = asyncio.Queue() 

39 

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 

43 

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") 

57 

58 async def send(self, msg): 

59 from js import Blob, Uint8Array 

60 

61 blob = Blob.new([Uint8Array.new(msg)]) 

62 self._socket.send(blob) 

63 

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) 

71 

72 def on_message(self, event): 

73 self._queue.put_nowait(("message", event)) 

74 

75 def on_error(self, event): 

76 self.open = False 

77 self._queue.put_nowait(("error", event)) 

78 

79 def on_close(self, event): 

80 self.open = False 

81 self._queue.put_nowait(("close", event)) 

82 

83 

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 

89 

90 if ssl is not None: 

91 raise ValueError("SSL can not be configured within the browser WebSocket API") 

92 

93 socket = WebSocket.new(uri, subprotocols) 

94 

95 # Ignoring ping_interval: We can't tell what the browser does, and it may 

96 # be right nor not. 

97 

98 proto = WebSocketClientProtocol(socket) 

99 

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 

110 

111 return proto 

112 

113 

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.""" 

117 

118 class WebSocketException(Exception): 

119 pass 

120 

121 class ConnectionClosed(WebSocketException): 

122 pass 

123 

124 

125# Mocks required by the aiocoap.transports.ws module expecting a full implementation 

126 

127 

128class WebSocketServerProtocol: 

129 def __init__(self, *args, **kwargs): 

130 raise RuntimeError("Web sockets in web browsers can not be used as servers") 

131 

132 

133WebSocketServer = WebSocketServerProtocol 

134 

135 

136def serve(*args, **kwargs): 

137 WebSocketServer()