Bläddra i källkod

feat: support session reconnection using `reconnect_timeout` parameter

wangweimin 4 år sedan
förälder
incheckning
2c68a45e6d

+ 26 - 17
docs/locales/zh_CN/LC_MESSAGES/platform.po

@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 msgstr ""
 "Project-Id-Version: PyWebIO 1.1.0\n"
 "Project-Id-Version: PyWebIO 1.1.0\n"
 "Report-Msgid-Bugs-To: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-03-14 21:20+0800\n"
-"PO-Revision-Date: 2021-03-14 21:31+0800\n"
+"POT-Creation-Date: 2021-03-16 22:45+0800\n"
+"PO-Revision-Date: 2021-03-16 23:55+0800\n"
 "Last-Translator: WangWeimin <wang0.618@qq.com>\n"
 "Last-Translator: WangWeimin <wang0.618@qq.com>\n"
 "Language: zh_CN\n"
 "Language: zh_CN\n"
 "Language-Team: \n"
 "Language-Team: \n"
@@ -114,7 +114,16 @@ msgstr ""
 "例如 ``http_static_dir`` 路径下存在文件 ``A/B.jpg`` ,则其URL为 ``http://<host>:"
 "例如 ``http_static_dir`` 路径下存在文件 ``A/B.jpg`` ,则其URL为 ``http://<host>:"
 "<port>/static/A/B.jpg``。"
 "<port>/static/A/B.jpg``。"
 
 
-#: of pywebio.platform.path_deploy:17
+#: of pywebio.platform.path_deploy:16 pywebio.platform.tornado.start_server:31
+msgid ""
+"The client can reconnect to server within ``reconnect_timeout`` seconds after an "
+"unexpected disconnection. If set to 0 (default), once the client disconnects, the "
+"server session will be closed."
+msgstr ""
+"客户端重连超时时间(秒)。客户端若意外与服务端断开连接,在 ``reconnect_timeout`` 秒内"
+"可以重新连接并自动恢复会话。"
+
+#: of pywebio.platform.path_deploy:18
 msgid ""
 msgid ""
 "The rest arguments of ``path_deploy()`` have the same meaning as for :func:"
 "The rest arguments of ``path_deploy()`` have the same meaning as for :func:"
 "`pywebio.platform.tornado.start_server`"
 "`pywebio.platform.tornado.start_server`"
@@ -297,7 +306,7 @@ msgstr ""
 "例如 ``http_static_dir`` 路径下存在文件 ``A/B.jpg`` ,则其URL为 ``http://<host>:"
 "例如 ``http_static_dir`` 路径下存在文件 ``A/B.jpg`` ,则其URL为 ``http://<host>:"
 "<port>/static/A/B.jpg``。"
 "<port>/static/A/B.jpg``。"
 
 
-#: of pywebio.platform.tornado.start_server:31
+#: of pywebio.platform.tornado.start_server:33
 msgid ""
 msgid ""
 "The allowed request source list. (The current server host is always allowed) The "
 "The allowed request source list. (The current server host is always allowed) The "
 "source contains the protocol, domain name, and port part. Can use Unix shell-style "
 "source contains the protocol, domain name, and port part. Can use Unix shell-style "
@@ -307,7 +316,7 @@ msgid ""
 "detail, see `Python Doc <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_"
 "detail, see `Python Doc <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_"
 msgstr ""
 msgstr ""
 
 
-#: of pywebio.platform.tornado.start_server:31
+#: of pywebio.platform.tornado.start_server:33
 msgid ""
 msgid ""
 "The allowed request source list. (The current server host is always allowed) The "
 "The allowed request source list. (The current server host is always allowed) The "
 "source contains the protocol, domain name, and port part. Can use Unix shell-style "
 "source contains the protocol, domain name, and port part. Can use Unix shell-style "
@@ -316,34 +325,34 @@ msgstr ""
 "除当前域名外,服务器还允许的请求的来源列表。来源包含协议、域名和端口部分,允许使用 "
 "除当前域名外,服务器还允许的请求的来源列表。来源包含协议、域名和端口部分,允许使用 "
 "Unix shell 风格的匹配模式:"
 "Unix shell 风格的匹配模式:"
 
 
-#: of pywebio.platform.tornado.start_server:35
+#: of pywebio.platform.tornado.start_server:37
 msgid "``*`` matches everything"
 msgid "``*`` matches everything"
 msgstr "``*`` 为通配符"
 msgstr "``*`` 为通配符"
 
 
-#: of pywebio.platform.tornado.start_server:36
+#: of pywebio.platform.tornado.start_server:38
 msgid "``?`` matches any single character"
 msgid "``?`` matches any single character"
 msgstr "``?`` 匹配单个字符"
 msgstr "``?`` 匹配单个字符"
 
 
-#: of pywebio.platform.tornado.start_server:37
+#: of pywebio.platform.tornado.start_server:39
 msgid "``[seq]`` matches any character in *seq*"
 msgid "``[seq]`` matches any character in *seq*"
 msgstr "``[seq]`` 匹配seq中的任何字符"
 msgstr "``[seq]`` 匹配seq中的任何字符"
 
 
-#: of pywebio.platform.tornado.start_server:38
+#: of pywebio.platform.tornado.start_server:40
 msgid "``[!seq]`` matches any character not in *seq*"
 msgid "``[!seq]`` matches any character not in *seq*"
 msgstr "``[!seq]`` 匹配任何不在seq中的字符"
 msgstr "``[!seq]`` 匹配任何不在seq中的字符"
 
 
-#: of pywebio.platform.tornado.start_server:40
+#: of pywebio.platform.tornado.start_server:42
 msgid "Such as: ``https://*.example.com`` 、 ``*://*.example.com``"
 msgid "Such as: ``https://*.example.com`` 、 ``*://*.example.com``"
 msgstr "比如 ``https://*.example.com`` 、 ``*://*.example.com``"
 msgstr "比如 ``https://*.example.com`` 、 ``*://*.example.com``"
 
 
-#: of pywebio.platform.tornado.start_server:42
+#: of pywebio.platform.tornado.start_server:44
 msgid ""
 msgid ""
 "For detail, see `Python Doc <https://docs.python.org/zh-tw/3/library/fnmatch."
 "For detail, see `Python Doc <https://docs.python.org/zh-tw/3/library/fnmatch."
 "html>`_"
 "html>`_"
 msgstr ""
 msgstr ""
 "全部规则参见 `Python文档 <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_ "
 "全部规则参见 `Python文档 <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_ "
 
 
-#: of pywebio.platform.tornado.start_server:43
+#: of pywebio.platform.tornado.start_server:45
 msgid ""
 msgid ""
 "The validation function for request source. It receives the source string (which "
 "The validation function for request source. It receives the source string (which "
 "contains protocol, host, and port parts) as parameter and return ``True/False`` to "
 "contains protocol, host, and port parts) as parameter and return ``True/False`` to "
@@ -354,13 +363,13 @@ msgstr ""
 "返回 ``True/False`` 指示服务器接受/拒绝该请求。若设置了 ``check_origin`` , "
 "返回 ``True/False`` 指示服务器接受/拒绝该请求。若设置了 ``check_origin`` , "
 "``allowed_origins`` 参数将被忽略"
 "``allowed_origins`` 参数将被忽略"
 
 
-#: of pywebio.platform.tornado.start_server:46
+#: of pywebio.platform.tornado.start_server:48
 msgid ""
 msgid ""
 "Whether or not auto open web browser when server is started (if the operating "
 "Whether or not auto open web browser when server is started (if the operating "
 "system allows it) ."
 "system allows it) ."
 msgstr "当服务启动后,是否自动打开浏览器来访问服务。(该操作需要操作系统支持)"
 msgstr "当服务启动后,是否自动打开浏览器来访问服务。(该操作需要操作系统支持)"
 
 
-#: of pywebio.platform.tornado.start_server:47
+#: of pywebio.platform.tornado.start_server:49
 msgid ""
 msgid ""
 "Max bytes of a message which Tornado can accept. Messages larger than the "
 "Max bytes of a message which Tornado can accept. Messages larger than the "
 "``websocket_max_message_size`` (default 10MB) will not be accepted. "
 "``websocket_max_message_size`` (default 10MB) will not be accepted. "
@@ -369,7 +378,7 @@ msgid ""
 "gigabytes, respectively). E.g: ``500``, ``'40K'``, ``'3M'``"
 "gigabytes, respectively). E.g: ``500``, ``'40K'``, ``'3M'``"
 msgstr ""
 msgstr ""
 
 
-#: of pywebio.platform.tornado.start_server:52
+#: of pywebio.platform.tornado.start_server:54
 msgid ""
 msgid ""
 "If set to a number, all websockets will be pinged every n seconds. This can help "
 "If set to a number, all websockets will be pinged every n seconds. This can help "
 "keep the connection alive through certain proxy servers which close idle "
 "keep the connection alive through certain proxy servers which close idle "
@@ -382,7 +391,7 @@ msgstr ""
 "WebSockets连接被代理服务器当作空闲连接而关闭。\n"
 "WebSockets连接被代理服务器当作空闲连接而关闭。\n"
 "同时,若WebSockets连接在某些情况下被异常关闭,应用也可以及时感知。"
 "同时,若WebSockets连接在某些情况下被异常关闭,应用也可以及时感知。"
 
 
-#: of pywebio.platform.tornado.start_server:55
+#: of pywebio.platform.tornado.start_server:57
 msgid ""
 msgid ""
 "If the ping interval is set, and the server doesn’t receive a ‘pong’ in this many "
 "If the ping interval is set, and the server doesn’t receive a ‘pong’ in this many "
 "seconds, it will close the websocket. The default is three times the ping "
 "seconds, it will close the websocket. The default is three times the ping "
@@ -394,7 +403,7 @@ msgstr ""
 "内收到'pong'消息,应用会将连接关闭。默认的超时时间为 ``websocket_ping_interval`` 的"
 "内收到'pong'消息,应用会将连接关闭。默认的超时时间为 ``websocket_ping_interval`` 的"
 "三倍。"
 "三倍。"
 
 
-#: of pywebio.platform.tornado.start_server:58
+#: of pywebio.platform.tornado.start_server:60
 msgid ""
 msgid ""
 "Additional keyword arguments passed to the constructor of ``tornado.web."
 "Additional keyword arguments passed to the constructor of ``tornado.web."
 "Application``. For details, please refer: https://www.tornadoweb.org/en/stable/web."
 "Application``. For details, please refer: https://www.tornadoweb.org/en/stable/web."

+ 217 - 203
docs/locales/zh_CN/LC_MESSAGES/spec.po

@@ -7,16 +7,17 @@ msgid ""
 msgstr ""
 msgstr ""
 "Project-Id-Version: PyWebIO 1.1.0\n"
 "Project-Id-Version: PyWebIO 1.1.0\n"
 "Report-Msgid-Bugs-To: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-18 11:52+0800\n"
-"PO-Revision-Date: 2021-02-18 10:50+0800\n"
+"POT-Creation-Date: 2021-03-16 22:45+0800\n"
+"PO-Revision-Date: 2021-03-16 23:55+0800\n"
 "Last-Translator: WangWeimin <wang0.618@qq.com>\n"
 "Last-Translator: WangWeimin <wang0.618@qq.com>\n"
 "Language: zh_CN\n"
 "Language: zh_CN\n"
 "Language-Team: \n"
 "Language-Team: \n"
-"Plural-Forms: nplurals=1; plural=0\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
 "MIME-Version: 1.0\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Generated-By: Babel 2.8.0\n"
 "Generated-By: Babel 2.8.0\n"
+"X-Generator: Poedit 2.4.2\n"
 
 
 #: ../../spec.rst:2
 #: ../../spec.rst:2
 msgid "Server-Client communication protocol"
 msgid "Server-Client communication protocol"
@@ -24,26 +25,28 @@ msgstr "服务器-客户端通信协议"
 
 
 #: ../../spec.rst:4
 #: ../../spec.rst:4
 msgid ""
 msgid ""
-"PyWebIO uses a server-client architecture, the server executes task code,"
-" and interacts with the client (that is, the user browser) through the "
-"network. This section introduce the protocol specification for the "
-"communication between PyWebIO server and client."
-msgstr "PyWebIO采用服务器-客户端架构,服务端运行任务代码,通过网络与客户端(也就是用户浏览器)交互。本章介绍PyWebIO服务端与客户端通信的协议。"
+"PyWebIO uses a server-client architecture, the server executes task code, and "
+"interacts with the client (that is, the user browser) through the network. This "
+"section introduce the protocol specification for the communication between "
+"PyWebIO server and client."
+msgstr ""
+"PyWebIO采用服务器-客户端架构,服务端运行任务代码,通过网络与客户端(也就是用户浏"
+"览器)交互。本章介绍PyWebIO服务端与客户端通信的协议。"
 
 
 #: ../../spec.rst:6
 #: ../../spec.rst:6
 msgid ""
 msgid ""
-"There are two communication methods between server and client: WebSocket "
-"and Http."
+"There are two communication methods between server and client: WebSocket and "
+"Http."
 msgstr "服务器与客户端有两种通信方式:WebSocket 和 Http 通信。"
 msgstr "服务器与客户端有两种通信方式:WebSocket 和 Http 通信。"
 
 
 #: ../../spec.rst:8
 #: ../../spec.rst:8
 msgid ""
 msgid ""
-"When using Tornado or aiohttp backend, the server and client communicate "
-"through WebSocket, when using Flask or Django backend, the server and "
-"client communicate through Http."
+"When using Tornado or aiohttp backend, the server and client communicate through "
+"WebSocket, when using Flask or Django backend, the server and client communicate "
+"through Http."
 msgstr ""
 msgstr ""
-"使用 Tornado或aiohttp 后端时,服务器与客户端通过 WebSocket 通信,使用 Flask或Django "
-"后端时,服务器与客户端通过 Http 通信。"
+"使用 Tornado或aiohttp 后端时,服务器与客户端通过 WebSocket 通信,使用 Flask或"
+"Django 后端时,服务器与客户端通过 Http 通信。"
 
 
 #: ../../spec.rst:10
 #: ../../spec.rst:10
 msgid "**WebSocket communication**"
 msgid "**WebSocket communication**"
@@ -61,21 +64,24 @@ msgstr "**Http 通信:**"
 
 
 #: ../../spec.rst:16
 #: ../../spec.rst:16
 msgid ""
 msgid ""
-"The client polls the backend through Http GET requests, and the backend "
-"returns a list of PyWebIO messages serialized in json"
-msgstr "* 客户端通过Http GET请求向后端轮询,后端返回json序列化之后的PyWebIO消息列表"
+"The client polls the backend through Http GET requests, and the backend returns "
+"a list of PyWebIO messages serialized in json"
+msgstr ""
+"* 客户端通过Http GET请求向后端轮询,后端返回json序列化之后的PyWebIO消息列表"
 
 
 #: ../../spec.rst:18
 #: ../../spec.rst:18
 msgid ""
 msgid ""
-"When the user submits the form or clicks the button, the client submits "
-"data to the backend through Http POST request"
+"When the user submits the form or clicks the button, the client submits data to "
+"the backend through Http POST request"
 msgstr "* 当用户提交表单或者点击页面按钮后,客户端通过Http POST请求向后端提交数据"
 msgstr "* 当用户提交表单或者点击页面按钮后,客户端通过Http POST请求向后端提交数据"
 
 
 #: ../../spec.rst:20
 #: ../../spec.rst:20
 msgid ""
 msgid ""
-"In the following, the data sent by the server to the client is called "
-"command, and the data sent by the client to the server is called event."
-msgstr "为方便区分,下文将由服务器向客户端发送的数据称作command,将客户端发向服务器的数据称作event"
+"In the following, the data sent by the server to the client is called command, "
+"and the data sent by the client to the server is called event."
+msgstr ""
+"为方便区分,下文将由服务器向客户端发送的数据称作command,将客户端发向服务器的数据"
+"称作event"
 
 
 #: ../../spec.rst:22
 #: ../../spec.rst:22
 msgid "The following describes the format of command and event"
 msgid "The following describes the format of command and event"
@@ -87,8 +93,7 @@ msgstr ""
 
 
 #: ../../spec.rst:27
 #: ../../spec.rst:27
 msgid ""
 msgid ""
-"Command is sent by the server to the client. The basic format of command "
-"is::"
+"Command is sent by the server to the client. The basic format of command is::"
 msgstr "command由服务器->客户端,基本格式为::"
 msgstr "command由服务器->客户端,基本格式为::"
 
 
 #: ../../spec.rst:29
 #: ../../spec.rst:29
@@ -110,20 +115,22 @@ msgstr "``command`` 字段表示指令名"
 
 
 #: ../../spec.rst:39
 #: ../../spec.rst:39
 msgid "``task_id`` : Id of the task that send the command"
 msgid "``task_id`` : Id of the task that send the command"
-msgstr "``task_id`` 字段表示发送指令的Task id,客户端对于此命令的响应事件都会传递 task_id"
+msgstr ""
+"``task_id`` 字段表示发送指令的Task id,客户端对于此命令的响应事件都会传递 task_id"
 
 
 #: ../../spec.rst:41
 #: ../../spec.rst:41
 msgid ""
 msgid ""
-"``spec`` : the data of the command, which is different depending on the "
-"command name"
+"``spec`` : the data of the command, which is different depending on the command "
+"name"
 msgstr "``spec`` 字段为指令的参数,不同指令参数不同"
 msgstr "``spec`` 字段为指令的参数,不同指令参数不同"
 
 
 #: ../../spec.rst:43
 #: ../../spec.rst:43
 msgid ""
 msgid ""
-"Note that: the arguments shown above are merely the same with the "
-"parameters of corresponding PyWebIO functions, but there are some "
-"differences."
-msgstr "需要注意,以下不同命令的参数和 PyWebIO 的对应函数的参数大部分含义一致,但是也有些许不同。"
+"Note that: the arguments shown above are merely the same with the parameters of "
+"corresponding PyWebIO functions, but there are some differences."
+msgstr ""
+"需要注意,以下不同命令的参数和 PyWebIO 的对应函数的参数大部分含义一致,但是也有些"
+"许不同。"
 
 
 #: ../../spec.rst:46
 #: ../../spec.rst:46
 msgid "The following describes the ``spec`` fields of different commands:"
 msgid "The following describes the ``spec`` fields of different commands:"
@@ -203,20 +210,19 @@ msgstr "表单是否可以取消"
 
 
 #: ../../spec.rst
 #: ../../spec.rst
 msgid ""
 msgid ""
-"If cancelable=True, a “Cancel” button will be displayed at the bottom of "
-"the form."
+"If cancelable=True, a “Cancel” button will be displayed at the bottom of the "
+"form."
 msgstr "若 ``cancelable=True`` 则会在表单底部显示一个”取消”按钮,"
 msgstr "若 ``cancelable=True`` 则会在表单底部显示一个”取消”按钮,"
 
 
 #: ../../spec.rst
 #: ../../spec.rst
 msgid ""
 msgid ""
-"A ``from_cancel`` event is triggered after the user clicks the cancel "
-"button."
+"A ``from_cancel`` event is triggered after the user clicks the cancel button."
 msgstr "用户点击取消按钮后,触发 ``from_cancel`` 事件"
 msgstr "用户点击取消按钮后,触发 ``from_cancel`` 事件"
 
 
 #: ../../spec.rst:77
 #: ../../spec.rst:77
 msgid ""
 msgid ""
-"The ``inputs`` field is a list of input items, each input item is a "
-"``dict``, the fields of the item are as follows:"
+"The ``inputs`` field is a list of input items, each input item is a ``dict``, "
+"the fields of the item are as follows:"
 msgstr "``inputs`` 字段为输入项组成的列表,每一输入项为一个 ``dict``,字段如下:"
 msgstr "``inputs`` 字段为输入项组成的列表,每一输入项为一个 ``dict``,字段如下:"
 
 
 #: ../../spec.rst:79
 #: ../../spec.rst:79
@@ -233,8 +239,8 @@ msgstr "name: 输入项id。必选"
 
 
 #: ../../spec.rst:82
 #: ../../spec.rst:82
 msgid ""
 msgid ""
-"auto_focus: Set focus automatically. At most one item of ``auto_focus`` "
-"can be true in the input item list"
+"auto_focus: Set focus automatically. At most one item of ``auto_focus`` can be "
+"true in the input item list"
 msgstr "auto_focus: 自动获取输入焦点. 输入项列表中最多只能由一项的auto_focus为真"
 msgstr "auto_focus: 自动获取输入焦点. 输入项列表中最多只能由一项的auto_focus为真"
 
 
 #: ../../spec.rst:83
 #: ../../spec.rst:83
@@ -315,14 +321,13 @@ msgstr ""
 
 
 #: ../../spec.rst:106
 #: ../../spec.rst:106
 msgid ""
 msgid ""
-"select: select  https://developer.mozilla.org/zh-"
-"CN/docs/Web/HTML/Element/select"
+"select: select  https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/select"
 msgstr ""
 msgstr ""
 
 
 #: ../../spec.rst:107
 #: ../../spec.rst:107
 msgid ""
 msgid ""
-"textarea: textarea  https://developer.mozilla.org/zh-"
-"CN/docs/Web/HTML/Element/textarea"
+"textarea: textarea  https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/"
+"textarea"
 msgstr ""
 msgstr ""
 
 
 #: ../../spec.rst:108
 #: ../../spec.rst:108
@@ -331,8 +336,8 @@ msgstr ""
 
 
 #: ../../spec.rst:109
 #: ../../spec.rst:109
 msgid ""
 msgid ""
-"actions: button[type=submit] https://developer.mozilla.org/zh-"
-"CN/docs/Web/HTML/Element/button"
+"actions: button[type=submit] https://developer.mozilla.org/zh-CN/docs/Web/HTML/"
+"Element/button"
 msgstr ""
 msgstr ""
 
 
 #: ../../spec.rst:111
 #: ../../spec.rst:111
@@ -341,14 +346,13 @@ msgstr ""
 
 
 #: ../../spec.rst:113
 #: ../../spec.rst:113
 msgid ""
 msgid ""
-"text,number,password: * action: Display a button on the right of the "
-"input field."
+"text,number,password: * action: Display a button on the right of the input field."
 msgstr ""
 msgstr ""
 
 
 #: ../../spec.rst:115
 #: ../../spec.rst:115
 msgid ""
 msgid ""
-"The format of ``action`` is ``{label: button label, callback_id: button "
-"click callback id}``"
+"The format of ``action`` is ``{label: button label, callback_id: button click "
+"callback id}``"
 msgstr ""
 msgstr ""
 
 
 #: ../../spec.rst:117
 #: ../../spec.rst:117
@@ -357,8 +361,8 @@ msgstr ""
 
 
 #: ../../spec.rst:119
 #: ../../spec.rst:119
 msgid ""
 msgid ""
-"code: Codemirror options, same as ``code`` parameter of "
-":func:`pywebio.input.textarea`"
+"code: Codemirror options, same as ``code`` parameter of :func:`pywebio.input."
+"textarea`"
 msgstr "code: Codemirror 参数, 见 :func:`pywebio.input.textarea` 的 ``code`` 参数"
 msgstr "code: Codemirror 参数, 见 :func:`pywebio.input.textarea` 的 ``code`` 参数"
 
 
 #: ../../spec.rst:121
 #: ../../spec.rst:121
@@ -387,8 +391,7 @@ msgstr ""
 
 
 #: ../../spec.rst:137
 #: ../../spec.rst:137
 msgid ""
 msgid ""
-"buttons: ``{label:, value:, [type: 'submit'/'reset'/'cancel'], "
-"[disabled:]}`` ."
+"buttons: ``{label:, value:, [type: 'submit'/'reset'/'cancel'], [disabled:]}`` ."
 msgstr ""
 msgstr ""
 
 
 #: ../../spec.rst:140
 #: ../../spec.rst:140
@@ -413,8 +416,8 @@ msgstr ""
 
 
 #: ../../spec.rst:149
 #: ../../spec.rst:149
 msgid ""
 msgid ""
-"Update the input item, you can update the ``spec`` of the input item of "
-"the currently displayed form"
+"Update the input item, you can update the ``spec`` of the input item of the "
+"currently displayed form"
 msgstr "更新输入项,用于对当前显示表单中输入项的 ``spec`` 进行更新"
 msgstr "更新输入项,用于对当前显示表单中输入项的 ``spec`` 进行更新"
 
 
 #: ../../spec.rst:151
 #: ../../spec.rst:151
@@ -427,8 +430,8 @@ msgstr ""
 
 
 #: ../../spec.rst:154
 #: ../../spec.rst:154
 msgid ""
 msgid ""
-"target_value: str, optional. Used to filter options in checkbox, radio, "
-"actions type"
+"target_value: str, optional. Used to filter options in checkbox, radio, actions "
+"type"
 msgstr ""
 msgstr ""
 
 
 #: ../../spec.rst:155
 #: ../../spec.rst:155
@@ -437,8 +440,8 @@ msgstr ""
 
 
 #: ../../spec.rst:157
 #: ../../spec.rst:157
 msgid ""
 msgid ""
-"valid_status: When it is bool, it means setting the state of the input "
-"value, pass/fail; when it is 0, it means clear the valid_status flag"
+"valid_status: When it is bool, it means setting the state of the input value, "
+"pass/fail; when it is 0, it means clear the valid_status flag"
 msgstr ""
 msgstr ""
 
 
 #: ../../spec.rst:158
 #: ../../spec.rst:158
@@ -467,363 +470,379 @@ msgstr ""
 
 
 #: ../../spec.rst:167
 #: ../../spec.rst:167
 msgid ""
 msgid ""
-"Indicates that the server has closed the connection. ``spec`` of the "
-"command is empty."
+"Indicates that the server has closed the connection. ``spec`` of the command is "
+"empty."
 msgstr "指示服务器端已经关闭连接。 ``spec`` 为空"
 msgstr "指示服务器端已经关闭连接。 ``spec`` 为空"
 
 
+#: ../../spec.rst:170
+msgid "set_session_id"
+msgstr ""
+
 #: ../../spec.rst:171
 #: ../../spec.rst:171
+msgid ""
+"Send current session id to client, used to reconnect to server (Only available "
+"in websocket connection). ``spec`` of the command is session id."
+msgstr ""
+"将当前会话id发送至客户端,客户端可以使用此id来重连会话(仅在websocket连接中可"
+"用)。 ``spec`` 字段为会话id"
+
+#: ../../spec.rst:175
 msgid "destroy_form"
 msgid "destroy_form"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:172
+#: ../../spec.rst:176
 msgid "Destroy the current form. ``spec`` of the command is empty."
 msgid "Destroy the current form. ``spec`` of the command is empty."
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:174
+#: ../../spec.rst:178
 msgid ""
 msgid ""
-"Note: The form will not be automatically destroyed after it is submitted,"
-" it needs to be explicitly destroyed using this command"
+"Note: The form will not be automatically destroyed after it is submitted, it "
+"needs to be explicitly destroyed using this command"
 msgstr "表单在页面上提交之后不会自动销毁,需要使用此命令显式销毁"
 msgstr "表单在页面上提交之后不会自动销毁,需要使用此命令显式销毁"
 
 
-#: ../../spec.rst:177
+#: ../../spec.rst:181
 msgid "output"
 msgid "output"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:178
+#: ../../spec.rst:182
 msgid "Output content"
 msgid "Output content"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:180
+#: ../../spec.rst:184
 msgid "The ``spec`` fields of ``output`` commands:"
 msgid "The ``spec`` fields of ``output`` commands:"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:182
+#: ../../spec.rst:186
 msgid "type: content type"
 msgid "type: content type"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:183
+#: ../../spec.rst:187
 msgid "style: str, Additional css style"
 msgid "style: str, Additional css style"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:184
+#: ../../spec.rst:188
 msgid ""
 msgid ""
-"scope: str, CSS selector of the output container. If multiple containers "
-"are matched, the content will be output to every matched container"
-msgstr "scope: str, 内容输出的域的css选择器。若CSS选择器匹配到页面上的多个容器,则内容会输出到每个匹配到的容器"
+"scope: str, CSS selector of the output container. If multiple containers are "
+"matched, the content will be output to every matched container"
+msgstr ""
+"scope: str, 内容输出的域的css选择器。若CSS选择器匹配到页面上的多个容器,则内容会"
+"输出到每个匹配到的容器"
 
 
-#: ../../spec.rst:185
+#: ../../spec.rst:189
 msgid "position: int, see :ref:`scope - User manual <scope_param>`"
 msgid "position: int, see :ref:`scope - User manual <scope_param>`"
 msgstr "int, 在输出域中输出的位置, 见 :ref:`输出函数的scope相关参数 <scope_param>`"
 msgstr "int, 在输出域中输出的位置, 见 :ref:`输出函数的scope相关参数 <scope_param>`"
 
 
-#: ../../spec.rst:186
+#: ../../spec.rst:190
 msgid "Other attributes of different types"
 msgid "Other attributes of different types"
 msgstr "不同type时的特有字段"
 msgstr "不同type时的特有字段"
 
 
-#: ../../spec.rst:188
+#: ../../spec.rst:192
 msgid "Unique attributes of different types:"
 msgid "Unique attributes of different types:"
 msgstr "``type`` 的可选值及特有字段:"
 msgstr "``type`` 的可选值及特有字段:"
 
 
-#: ../../spec.rst:190
+#: ../../spec.rst:194
 msgid "type: markdown"
 msgid "type: markdown"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:192 ../../spec.rst:198 ../../spec.rst:203
+#: ../../spec.rst:196 ../../spec.rst:202 ../../spec.rst:207
 msgid "content: str"
 msgid "content: str"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:193
+#: ../../spec.rst:197
 msgid "options: dict, `marked.js <https://github.com/markedjs/marked>`_ options"
 msgid "options: dict, `marked.js <https://github.com/markedjs/marked>`_ options"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:194 ../../spec.rst:199
+#: ../../spec.rst:198 ../../spec.rst:203
 msgid "sanitize: bool, Whether to enable a XSS sanitizer for HTML"
 msgid "sanitize: bool, Whether to enable a XSS sanitizer for HTML"
 msgstr ""
 msgstr ""
-"sanitize: bool, 是否使用 `DOMPurify <https://github.com/cure53/DOMPurify>`_ "
-"对内容进行过滤来防止XSS攻击。"
+"sanitize: bool, 是否使用 `DOMPurify <https://github.com/cure53/DOMPurify>`_ 对内"
+"容进行过滤来防止XSS攻击。"
 
 
-#: ../../spec.rst:196
+#: ../../spec.rst:200
 msgid "type: html"
 msgid "type: html"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:201
+#: ../../spec.rst:205
 msgid "type: text"
 msgid "type: text"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:204
+#: ../../spec.rst:208
 msgid ""
 msgid ""
-"inline: bool, Use text as an inline element (no line break at the end of "
-"the text)"
+"inline: bool, Use text as an inline element (no line break at the end of the "
+"text)"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:206
+#: ../../spec.rst:210
 msgid "type: buttons"
 msgid "type: buttons"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:208
+#: ../../spec.rst:212
 msgid "callback_id:"
 msgid "callback_id:"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:209
+#: ../../spec.rst:213
 msgid "buttons:[ {value:, label:, [color:]},...]"
 msgid "buttons:[ {value:, label:, [color:]},...]"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:210
+#: ../../spec.rst:214
 msgid "small: bool, Whether to enable small button"
 msgid "small: bool, Whether to enable small button"
 msgstr "small: bool,是否显示为小按钮样式"
 msgstr "small: bool,是否显示为小按钮样式"
 
 
-#: ../../spec.rst:211
+#: ../../spec.rst:215
 msgid "link: bool, Whether to make button seem as link."
 msgid "link: bool, Whether to make button seem as link."
 msgstr "link: bool,是否显示为链接样式"
 msgstr "link: bool,是否显示为链接样式"
 
 
-#: ../../spec.rst:213
+#: ../../spec.rst:217
 msgid "type: file"
 msgid "type: file"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:215
+#: ../../spec.rst:219
 msgid "name: File name when downloading"
 msgid "name: File name when downloading"
 msgstr "name: 下载保存为的文件名"
 msgstr "name: 下载保存为的文件名"
 
 
-#: ../../spec.rst:216
+#: ../../spec.rst:220
 msgid "content: File content with base64 encoded"
 msgid "content: File content with base64 encoded"
 msgstr "content: 文件base64编码的内容"
 msgstr "content: 文件base64编码的内容"
 
 
-#: ../../spec.rst:218
+#: ../../spec.rst:222
 msgid "type: table"
 msgid "type: table"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:220
+#: ../../spec.rst:224
 msgid ""
 msgid ""
-"data: Table data, which is a two-dimensional list, the first row is table"
-" header."
+"data: Table data, which is a two-dimensional list, the first row is table header."
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:221
+#: ../../spec.rst:225
 msgid ""
 msgid ""
-"span: cell span info. Format: {\"[row id],[col id]\": {\"row\":row span, "
-"\"col\":col span }}"
+"span: cell span info. Format: {\"[row id],[col id]\": {\"row\":row span, \"col\":"
+"col span }}"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:224
+#: ../../spec.rst:228
 msgid "popup"
 msgid "popup"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:225
+#: ../../spec.rst:229
 msgid "Show popup"
 msgid "Show popup"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:227 ../../spec.rst:240
+#: ../../spec.rst:231 ../../spec.rst:244
 msgid "The ``spec`` fields of ``popup`` commands:"
 msgid "The ``spec`` fields of ``popup`` commands:"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:229
+#: ../../spec.rst:233
 msgid "title"
 msgid "title"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:230 ../../spec.rst:242
+#: ../../spec.rst:234 ../../spec.rst:246
 msgid "content"
 msgid "content"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:231
+#: ../../spec.rst:235
 msgid "size: ``large``, ``normal``, ``small``"
 msgid "size: ``large``, ``normal``, ``small``"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:232
+#: ../../spec.rst:236
 msgid "implicit_close"
 msgid "implicit_close"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:233
+#: ../../spec.rst:237
 msgid "closable"
 msgid "closable"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:234
+#: ../../spec.rst:238
 msgid "dom_id: DOM id of popup container element"
 msgid "dom_id: DOM id of popup container element"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:237
+#: ../../spec.rst:241
 msgid "toast"
 msgid "toast"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:238
+#: ../../spec.rst:242
 msgid "Show a notification message"
 msgid "Show a notification message"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:243
+#: ../../spec.rst:247
 msgid "duration"
 msgid "duration"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:244
+#: ../../spec.rst:248
 msgid "position: `'left'` / `'center'` / `'right'`"
 msgid "position: `'left'` / `'center'` / `'right'`"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:245
+#: ../../spec.rst:249
 msgid "color: hexadecimal color value starting with '#'"
 msgid "color: hexadecimal color value starting with '#'"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:246
+#: ../../spec.rst:250
 msgid "callback_id"
 msgid "callback_id"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:250
+#: ../../spec.rst:254
 msgid "close_popup"
 msgid "close_popup"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:251
+#: ../../spec.rst:255
 msgid "Close the current popup window."
 msgid "Close the current popup window."
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:253
+#: ../../spec.rst:257
 msgid "``spec`` of the command is empty."
 msgid "``spec`` of the command is empty."
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:256
+#: ../../spec.rst:260
 msgid "set_env"
 msgid "set_env"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:257
+#: ../../spec.rst:261
 msgid "Config the environment of current session."
 msgid "Config the environment of current session."
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:259
+#: ../../spec.rst:263
 msgid "The ``spec`` fields of ``set_env`` commands:"
 msgid "The ``spec`` fields of ``set_env`` commands:"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:261
+#: ../../spec.rst:265
 msgid "title (str)"
 msgid "title (str)"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:262
+#: ../../spec.rst:266
 msgid "output_animation (bool)"
 msgid "output_animation (bool)"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:263
+#: ../../spec.rst:267
 msgid "auto_scroll_bottom (bool)"
 msgid "auto_scroll_bottom (bool)"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:264
+#: ../../spec.rst:268
 msgid "http_pull_interval (int)"
 msgid "http_pull_interval (int)"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:267
+#: ../../spec.rst:271
 msgid "output_ctl"
 msgid "output_ctl"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:268
+#: ../../spec.rst:272
 msgid "Output control"
 msgid "Output control"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:270
+#: ../../spec.rst:274
 msgid "The ``spec`` fields of ``output_ctl`` commands:"
 msgid "The ``spec`` fields of ``output_ctl`` commands:"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:272
+#: ../../spec.rst:276
 msgid "set_scope: scope name"
 msgid "set_scope: scope name"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:274
+#: ../../spec.rst:278
 msgid "container: Specify css selector to the parent scope of target scope."
 msgid "container: Specify css selector to the parent scope of target scope."
 msgstr "container: 新创建的scope的父scope的css选择器"
 msgstr "container: 新创建的scope的父scope的css选择器"
 
 
-#: ../../spec.rst:275
+#: ../../spec.rst:279
 msgid "position: int, The index where this scope is created in the parent scope."
 msgid "position: int, The index where this scope is created in the parent scope."
 msgstr "position: int, 在父scope中创建此scope的位置."
 msgstr "position: int, 在父scope中创建此scope的位置."
 
 
-#: ../../spec.rst:276
+#: ../../spec.rst:280
 msgid "if_exist: What to do when the specified scope already exists:"
 msgid "if_exist: What to do when the specified scope already exists:"
 msgstr "scope已经存在时如何操作:"
 msgstr "scope已经存在时如何操作:"
 
 
-#: ../../spec.rst:278
+#: ../../spec.rst:282
 msgid "null: Do nothing"
 msgid "null: Do nothing"
 msgstr "null/不指定: 表示立即返回不进行任何操作"
 msgstr "null/不指定: 表示立即返回不进行任何操作"
 
 
-#: ../../spec.rst:279
+#: ../../spec.rst:283
 msgid "`'remove'`: Remove the old scope first and then create a new one"
 msgid "`'remove'`: Remove the old scope first and then create a new one"
 msgstr "`’remove’` : 先移除旧scope再创建新scope"
 msgstr "`’remove’` : 先移除旧scope再创建新scope"
 
 
-#: ../../spec.rst:280
+#: ../../spec.rst:284
 msgid ""
 msgid ""
-"`'clear'`: Just clear the contents of the old scope, but don’t create a "
-"new scope"
+"`'clear'`: Just clear the contents of the old scope, but don’t create a new scope"
 msgstr "`’clear’` : 将旧scope的内容清除,不创建新scope"
 msgstr "`’clear’` : 将旧scope的内容清除,不创建新scope"
 
 
-#: ../../spec.rst:282
+#: ../../spec.rst:286
 msgid "clear: css selector of the scope need to clear"
 msgid "clear: css selector of the scope need to clear"
 msgstr "clear: 需要清空的scope的css选择器"
 msgstr "clear: 需要清空的scope的css选择器"
 
 
-#: ../../spec.rst:283
+#: ../../spec.rst:287
 msgid "clear_before"
 msgid "clear_before"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:284
+#: ../../spec.rst:288
 msgid "clear_after"
 msgid "clear_after"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:285
+#: ../../spec.rst:289
 msgid "clear_range:[,]"
 msgid "clear_range:[,]"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:286
+#: ../../spec.rst:290
 msgid "scroll_to"
 msgid "scroll_to"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:287
+#: ../../spec.rst:291
 msgid ""
 msgid ""
-"position: top/middle/bottom, Where to place the scope in the visible area"
-" of the page"
-msgstr "position: top/middle/bottom 与scroll_to一起出现, 表示滚动页面,让scope位于屏幕可视区域顶部/中部/底部"
+"position: top/middle/bottom, Where to place the scope in the visible area of the "
+"page"
+msgstr ""
+"position: top/middle/bottom 与scroll_to一起出现, 表示滚动页面,让scope位于屏幕可"
+"视区域顶部/中部/底部"
 
 
-#: ../../spec.rst:288
+#: ../../spec.rst:292
 msgid "remove: Remove the specified scope"
 msgid "remove: Remove the specified scope"
 msgstr "remove: 将给定的scope连同scope处的内容移除"
 msgstr "remove: 将给定的scope连同scope处的内容移除"
 
 
-#: ../../spec.rst:291
+#: ../../spec.rst:295
 msgid "run_script"
 msgid "run_script"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:292
+#: ../../spec.rst:296
 msgid "run javascript code in user's browser"
 msgid "run javascript code in user's browser"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:294
+#: ../../spec.rst:298
 msgid "The ``spec`` fields of ``run_script`` commands:"
 msgid "The ``spec`` fields of ``run_script`` commands:"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:296
+#: ../../spec.rst:300
 msgid "code: str, code"
 msgid "code: str, code"
 msgstr "code: 字符串格式的要运行的js代码"
 msgstr "code: 字符串格式的要运行的js代码"
 
 
-#: ../../spec.rst:297
+#: ../../spec.rst:301
 msgid "args: dict, Local variables passed to js code"
 msgid "args: dict, Local variables passed to js code"
-msgstr "args: 传递给代码的局部变量。字典类型,字典键表示变量名,字典值表示变量值(变量值需要可以被json序列化)"
+msgstr ""
+"args: 传递给代码的局部变量。字典类型,字典键表示变量名,字典值表示变量值(变量值需"
+"要可以被json序列化)"
 
 
-#: ../../spec.rst:300
+#: ../../spec.rst:304
 msgid "download"
 msgid "download"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:301
+#: ../../spec.rst:305
 msgid "Send file to user"
 msgid "Send file to user"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:303
+#: ../../spec.rst:307
 msgid "The ``spec`` fields of ``download`` commands:"
 msgid "The ``spec`` fields of ``download`` commands:"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:305
+#: ../../spec.rst:309
 msgid "name: str, File name when downloading"
 msgid "name: str, File name when downloading"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:306
+#: ../../spec.rst:310
 msgid "content: str, File content in base64 encoding."
 msgid "content: str, File content in base64 encoding."
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:309
+#: ../../spec.rst:313
 msgid "Event"
 msgid "Event"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:311
+#: ../../spec.rst:315
 msgid "Event is sent by the client to the server. The basic format of event is::"
 msgid "Event is sent by the client to the server. The basic format of event is::"
 msgstr "Event消息由客户端发往服务端。基本格式::"
 msgstr "Event消息由客户端发往服务端。基本格式::"
 
 
-#: ../../spec.rst:313
+#: ../../spec.rst:317
 msgid ""
 msgid ""
 "{\n"
 "{\n"
 "    event: event name\n"
 "    event: event name\n"
@@ -832,97 +851,92 @@ msgid ""
 "}"
 "}"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:319
+#: ../../spec.rst:323
 msgid ""
 msgid ""
-"The ``data`` field is the data carried by the event, and its content "
-"varies according to the event. The ``data`` field of different events is "
-"as follows:"
-msgstr "``event`` 表示事件名称。 ``data`` 为事件所携带的数据,其根据事件不同内容也会不同,不同事件对应的 ``data`` 字段如下:"
+"The ``data`` field is the data carried by the event, and its content varies "
+"according to the event. The ``data`` field of different events is as follows:"
+msgstr ""
+"``event`` 表示事件名称。 ``data`` 为事件所携带的数据,其根据事件不同内容也会不"
+"同,不同事件对应的 ``data`` 字段如下:"
 
 
-#: ../../spec.rst:323
+#: ../../spec.rst:327
 msgid "input_event"
 msgid "input_event"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:324
+#: ../../spec.rst:328
 msgid "Triggered when the form changes"
 msgid "Triggered when the form changes"
 msgstr "表单发生更改时触发"
 msgstr "表单发生更改时触发"
 
 
-#: ../../spec.rst:326
+#: ../../spec.rst:330
 msgid ""
 msgid ""
-"event_name: Current available value is ``'blur'``, which indicates that "
-"the input item loses focus"
+"event_name: Current available value is ``'blur'``, which indicates that the "
+"input item loses focus"
 msgstr "event_name: 目前可用值 ``’blur’``,表示输入项失去焦点"
 msgstr "event_name: 目前可用值 ``’blur’``,表示输入项失去焦点"
 
 
-#: ../../spec.rst:327
+#: ../../spec.rst:331
 msgid "name: name of input item"
 msgid "name: name of input item"
 msgstr "name: 输入项name"
 msgstr "name: 输入项name"
 
 
-#: ../../spec.rst:328
+#: ../../spec.rst:332
 msgid "value: value of input item"
 msgid "value: value of input item"
 msgstr "value: 输入项值"
 msgstr "value: 输入项值"
 
 
-#: ../../spec.rst:330
+#: ../../spec.rst:334
 msgid "note: checkbox and radio do not generate blur events"
 msgid "note: checkbox and radio do not generate blur events"
 msgstr "注意: checkbox radio 不产生blur事件"
 msgstr "注意: checkbox radio 不产生blur事件"
 
 
-#: ../../spec.rst:335
+#: ../../spec.rst:339
 msgid "callback"
 msgid "callback"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:336
+#: ../../spec.rst:340
 msgid "Triggered when the user clicks the button in the page"
 msgid "Triggered when the user clicks the button in the page"
 msgstr "用户点击显示区的按钮时触发"
 msgstr "用户点击显示区的按钮时触发"
 
 
-#: ../../spec.rst:338
+#: ../../spec.rst:342
 msgid ""
 msgid ""
-"In the ``callback`` event, ``task_id`` is the ``callback_id`` field of "
-"the ``button``; The ``data`` of the event is the ``value`` of the button "
-"that was clicked"
+"In the ``callback`` event, ``task_id`` is the ``callback_id`` field of the "
+"``button``; The ``data`` of the event is the ``value`` of the button that was "
+"clicked"
 msgstr ""
 msgstr ""
-"在 ``callback`` 事件中,``task_id`` 为对应的 ``button`` 组件的 ``callback_id`` 字段;\n"
+"在 ``callback`` 事件中,``task_id`` 为对应的 ``button`` 组件的 ``callback_id`` 字"
+"段;\n"
 "事件的 ``data`` 为被点击button的 ``value``"
 "事件的 ``data`` 为被点击button的 ``value``"
 
 
-#: ../../spec.rst:342
+#: ../../spec.rst:346
 msgid "from_submit"
 msgid "from_submit"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:343
+#: ../../spec.rst:347
 msgid "Triggered when the user submits the form"
 msgid "Triggered when the user submits the form"
 msgstr "用户提交表单时触发"
 msgstr "用户提交表单时触发"
 
 
-#: ../../spec.rst:345
+#: ../../spec.rst:349
 msgid ""
 msgid ""
-"The ``data`` of the event is a dict, whose key is the name of the input "
-"item, and whose value is the value of the input item."
+"The ``data`` of the event is a dict, whose key is the name of the input item, "
+"and whose value is the value of the input item."
 msgstr "事件 ``data`` 字段为表单 ``name`` -> 表单值 的字典"
 msgstr "事件 ``data`` 字段为表单 ``name`` -> 表单值 的字典"
 
 
-#: ../../spec.rst:348
+#: ../../spec.rst:352
 msgid "from_cancel"
 msgid "from_cancel"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:349
+#: ../../spec.rst:353
 msgid "Cancel input form"
 msgid "Cancel input form"
 msgstr "表单取消输入"
 msgstr "表单取消输入"
 
 
-#: ../../spec.rst:351
+#: ../../spec.rst:355
 msgid "The ``data`` of the event is ``None``"
 msgid "The ``data`` of the event is ``None``"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:354
+#: ../../spec.rst:358
 msgid "js_yield"
 msgid "js_yield"
 msgstr ""
 msgstr ""
 
 
-#: ../../spec.rst:355
+#: ../../spec.rst:359
 msgid "submit data from js"
 msgid "submit data from js"
 msgstr "js代码提交数据"
 msgstr "js代码提交数据"
 
 
-#: ../../spec.rst:357
+#: ../../spec.rst:361
 msgid "The ``data`` of the event is the data need to submit"
 msgid "The ``data`` of the event is the data need to submit"
 msgstr "事件 ``data`` 字段为相应的数据"
 msgstr "事件 ``data`` 字段为相应的数据"
-
-#~ msgid ""
-#~ "other fields of item's ``spec`` // "
-#~ "not support to inline adn label "
-#~ "fields"
-#~ msgstr ""
-

+ 4 - 0
docs/spec.rst

@@ -166,6 +166,10 @@ close_session
 ^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^
 Indicates that the server has closed the connection. ``spec`` of the command is empty.
 Indicates that the server has closed the connection. ``spec`` of the command is empty.
 
 
+set_session_id
+^^^^^^^^^^^^^^^
+Send current session id to client, used to reconnect to server (Only available in websocket connection).
+``spec`` of the command is session id.
 
 
 destroy_form
 destroy_form
 ^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^

+ 5 - 2
pywebio/platform/path_deploy.py

@@ -171,6 +171,7 @@ def _path_deploy(base, port=0, host='',
 
 
 def path_deploy(base, port=0, host='',
 def path_deploy(base, port=0, host='',
                 index=True, static_dir=None,
                 index=True, static_dir=None,
+                reconnect_timeout=0,
                 cdn=True, debug=True,
                 cdn=True, debug=True,
                 allowed_origins=None, check_origin=None,
                 allowed_origins=None, check_origin=None,
                 websocket_max_message_size=None,
                 websocket_max_message_size=None,
@@ -192,7 +193,8 @@ def path_deploy(base, port=0, host='',
        The files in this directory can be accessed via ``http://<host>:<port>/static/files``.
        The files in this directory can be accessed via ``http://<host>:<port>/static/files``.
        For example, if there is a ``A/B.jpg`` file in ``http_static_dir`` path,
        For example, if there is a ``A/B.jpg`` file in ``http_static_dir`` path,
        it can be accessed via ``http://<host>:<port>/static/A/B.jpg``.
        it can be accessed via ``http://<host>:<port>/static/A/B.jpg``.
-
+    :param int reconnect_timeout: The client can reconnect to server within ``reconnect_timeout`` seconds after an unexpected disconnection.
+       If set to 0 (default), once the client disconnects, the server session will be closed.
     The rest arguments of ``path_deploy()`` have the same meaning as for :func:`pywebio.platform.tornado.start_server`
     The rest arguments of ``path_deploy()`` have the same meaning as for :func:`pywebio.platform.tornado.start_server`
     """
     """
     gen = _path_deploy(base, port=port, host=host,
     gen = _path_deploy(base, port=port, host=host,
@@ -207,7 +209,8 @@ def path_deploy(base, port=0, host='',
 
 
     index_func = {True: partial(default_index_page, base=abs_base), False: lambda p: '403 Forbidden'}.get(index, index)
     index_func = {True: partial(default_index_page, base=abs_base), False: lambda p: '403 Forbidden'}.get(index, index)
 
 
-    Handler = webio_handler(lambda: None, cdn_url, allowed_origins=allowed_origins, check_origin=check_origin)
+    Handler = webio_handler(lambda: None, cdn_url, allowed_origins=allowed_origins,
+                            check_origin=check_origin, reconnect_timeout=reconnect_timeout)
 
 
     class WSHandler(Handler):
     class WSHandler(Handler):
 
 

+ 122 - 38
pywebio/platform/tornado.py

@@ -4,8 +4,10 @@ import json
 import logging
 import logging
 import os
 import os
 import threading
 import threading
+import time
 import webbrowser
 import webbrowser
 from functools import partial
 from functools import partial
+from typing import Dict
 from urllib.parse import urlparse
 from urllib.parse import urlparse
 
 
 import tornado
 import tornado
@@ -19,7 +21,7 @@ from ..session import CoroutineBasedSession, ThreadBasedSession, ScriptModeSessi
     register_session_implement_for_target, Session
     register_session_implement_for_target, Session
 from ..session.base import get_session_info_from_headers
 from ..session.base import get_session_info_from_headers
 from ..utils import get_free_port, wait_host_port, STATIC_PATH, iscoroutinefunction, isgeneratorfunction, \
 from ..utils import get_free_port, wait_host_port, STATIC_PATH, iscoroutinefunction, isgeneratorfunction, \
-    check_webio_js, parse_file_size
+    check_webio_js, parse_file_size, random_str, LRUDict
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -61,7 +63,7 @@ def _is_same_site(origin, handler: WebSocketHandler):
     return origin == host
     return origin == host
 
 
 
 
-def _webio_handler(applications=None, cdn=True, check_origin_func=_is_same_site):
+def _webio_handler(applications=None, cdn=True, reconnect_timeout=0, check_origin_func=_is_same_site):
     """
     """
     :param dict applications: dict of `name -> task function`
     :param dict applications: dict of `name -> task function`
     :param bool/str cdn: Whether to load front-end static resources from CDN
     :param bool/str cdn: Whether to load front-end static resources from CDN
@@ -74,6 +76,15 @@ def _webio_handler(applications=None, cdn=True, check_origin_func=_is_same_site)
         applications = dict(index=lambda: None)  # mock PyWebIO app
         applications = dict(index=lambda: None)  # mock PyWebIO app
 
 
     class WSHandler(WebSocketHandler):
     class WSHandler(WebSocketHandler):
+        def __init__(self, *args, **kwargs):
+            super().__init__(*args, **kwargs)
+            self._close_from_session = False
+            self.session_id = None
+            self.session = None  # type: Session
+            if reconnect_timeout and not type(self)._started_clean_task:
+                type(self)._started_clean_task = True
+                tornado.ioloop.IOLoop.current().call_later(reconnect_timeout // 2, type(self).clean_expired_sessions)
+                logger.debug("Started session clean task")
 
 
         def get_app(self):
         def get_app(self):
             app_name = self.get_query_argument('app', 'index')
             app_name = self.get_query_argument('app', 'index')
@@ -101,55 +112,115 @@ def _webio_handler(applications=None, cdn=True, check_origin_func=_is_same_site)
             # Non-None enables compression with default options.
             # Non-None enables compression with default options.
             return {}
             return {}
 
 
-        def send_msg_to_client(self, session: Session):
+        @classmethod
+        def clean_expired_sessions(cls):
+            tornado.ioloop.IOLoop.current().call_later(reconnect_timeout // 2, cls.clean_expired_sessions)
+
+            while cls._session_expire:
+                session_id, expire_ts = cls._session_expire.popitem(last=False)  # 弹出最早过期的session
+
+                if time.time() < expire_ts:
+                    # this session is not expired
+                    cls._session_expire[session_id] = expire_ts  # restore this item
+                    cls._session_expire.move_to_end(session_id, last=False)  # move to front
+                    break
+
+                # clean this session
+                logger.debug("session %s expired" % session_id)
+                cls._connections.pop(session_id, None)
+                session = cls._webio_sessions.pop(session_id, None)
+                if session:
+                    session.close(nonblock=True)
+
+        @classmethod
+        def send_msg_to_client(cls, _, session_id=None):
+            conn = cls._connections.get(session_id)
+            session = cls._webio_sessions[session_id]
+
+            if not conn or not conn.ws_connection:
+                return
+
             for msg in session.get_task_commands():
             for msg in session.get_task_commands():
-                self.write_message(json.dumps(msg))
+                conn.write_message(json.dumps(msg))
+
+        @classmethod
+        def close_from_session(cls, session_id=None):
+            cls.send_msg_to_client(None, session_id=session_id)
+
+            conn = cls._connections.pop(session_id, None)
+            cls._webio_sessions.pop(session_id, None)
+            if conn and conn.ws_connection:
+                conn._close_from_session = True
+                conn.close()
+
+        _started_clean_task = False
+        _session_expire = LRUDict()  # session_id -> expire timestamp. In increasing order of expire time
+        _webio_sessions = {}  # type: Dict[str, Session]  # session_id -> session
+        _connections = {}  # type: Dict[str, WSHandler]  # session_id -> WSHandler
 
 
         def open(self):
         def open(self):
             logger.debug("WebSocket opened")
             logger.debug("WebSocket opened")
-            # self.set_nodelay(True)
-
-            # 由session主动关闭连接
-            # connection is closed from session
-            self._close_from_session_tag = False
-
-            session_info = get_session_info_from_headers(self.request.headers)
-            session_info['user_ip'] = self.request.remote_ip
-            session_info['request'] = self.request
-            session_info['backend'] = 'tornado'
-            session_info['protocol'] = 'websocket'
-
-            application = self.get_app()
-            if iscoroutinefunction(application) or isgeneratorfunction(application):
-                self.session = CoroutineBasedSession(application, session_info=session_info,
-                                                     on_task_command=self.send_msg_to_client,
-                                                     on_session_close=self.close_from_session)
+            cls = type(self)
+
+            self.session_id = self.get_query_argument('session', None)
+            if self.session_id in ('NEW', None):  # 初始请求,创建新 Session
+                session_info = get_session_info_from_headers(self.request.headers)
+                session_info['user_ip'] = self.request.remote_ip
+                session_info['request'] = self.request
+                session_info['backend'] = 'tornado'
+                session_info['protocol'] = 'websocket'
+
+                application = self.get_app()
+                self.session_id = random_str(24)
+                cls._connections[self.session_id] = self
+
+                if iscoroutinefunction(application) or isgeneratorfunction(application):
+                    self.session = CoroutineBasedSession(
+                        application, session_info=session_info,
+                        on_task_command=partial(self.send_msg_to_client, session_id=self.session_id),
+                        on_session_close=partial(self.close_from_session, session_id=self.session_id))
+                else:
+                    self.session = ThreadBasedSession(
+                        application, session_info=session_info,
+                        on_task_command=partial(self.send_msg_to_client, session_id=self.session_id),
+                        on_session_close=partial(self.close_from_session, session_id=self.session_id),
+                        loop=asyncio.get_event_loop())
+                cls._webio_sessions[self.session_id] = self.session
+
+                if reconnect_timeout:
+                    self.write_message(json.dumps(dict(command='set_session_id', spec=self.session_id)))
+
+            elif self.session_id not in cls._webio_sessions:  # WebIOSession deleted
+                self.write_message(json.dumps(dict(command='close_session')))
             else:
             else:
-                self.session = ThreadBasedSession(application, session_info=session_info,
-                                                  on_task_command=self.send_msg_to_client,
-                                                  on_session_close=self.close_from_session,
-                                                  loop=asyncio.get_event_loop())
+                self.session = cls._webio_sessions[self.session_id]
+                cls._session_expire.pop(self.session_id, None)
+                cls._connections[self.session_id] = self
+                cls.send_msg_to_client(self.session, self.session_id)
+
+            logger.debug('session id: %s' % self.session_id)
 
 
         def on_message(self, message):
         def on_message(self, message):
             data = json.loads(message)
             data = json.loads(message)
             if data is not None:
             if data is not None:
                 self.session.send_client_event(data)
                 self.session.send_client_event(data)
 
 
-        def close_from_session(self):
-            self._close_from_session_tag = True
-            self.close()
-
         def on_close(self):
         def on_close(self):
-            # Session.close() is called only when connection is closed from the client.
-            # 只有在由客户端主动断开连接时,才调用 session.close()
-            if not self._close_from_session_tag:
+            cls = type(self)
+            cls._connections.pop(self.session_id, None)
+            if not reconnect_timeout and not self._close_from_session:
                 self.session.close(nonblock=True)
                 self.session.close(nonblock=True)
+            elif reconnect_timeout:
+                if self._close_from_session:
+                    cls._webio_sessions.pop(self.session_id, None)
+                elif self.session:
+                    cls._session_expire[self.session_id] = time.time() + reconnect_timeout
             logger.debug("WebSocket closed")
             logger.debug("WebSocket closed")
 
 
     return WSHandler
     return WSHandler
 
 
 
 
-def webio_handler(applications, cdn=True, allowed_origins=None, check_origin=None):
+def webio_handler(applications, cdn=True, reconnect_timeout=0, allowed_origins=None, check_origin=None):
     """Get the ``RequestHandler`` class for running PyWebIO applications in Tornado.
     """Get the ``RequestHandler`` class for running PyWebIO applications in Tornado.
     The ``RequestHandler`` communicates with the browser by WebSocket protocol.
     The ``RequestHandler`` communicates with the browser by WebSocket protocol.
 
 
@@ -166,7 +237,8 @@ def webio_handler(applications, cdn=True, allowed_origins=None, check_origin=Non
     else:
     else:
         check_origin_func = lambda origin, handler: _is_same_site(origin, handler) or check_origin(origin)
         check_origin_func = lambda origin, handler: _is_same_site(origin, handler) or check_origin(origin)
 
 
-    return _webio_handler(applications=applications, cdn=cdn, check_origin_func=check_origin_func)
+    return _webio_handler(applications=applications, cdn=cdn, check_origin_func=check_origin_func,
+                          reconnect_timeout=reconnect_timeout)
 
 
 
 
 async def open_webbrowser_on_server_started(host, port):
 async def open_webbrowser_on_server_started(host, port):
@@ -197,6 +269,7 @@ def _setup_server(webio_handler, port=0, host='', static_dir=None, **tornado_app
 
 
 def start_server(applications, port=0, host='',
 def start_server(applications, port=0, host='',
                  debug=False, cdn=True, static_dir=None,
                  debug=False, cdn=True, static_dir=None,
+                 reconnect_timeout=0,
                  allowed_origins=None, check_origin=None,
                  allowed_origins=None, check_origin=None,
                  auto_open_webbrowser=False,
                  auto_open_webbrowser=False,
                  websocket_max_message_size=None,
                  websocket_max_message_size=None,
@@ -233,6 +306,8 @@ def start_server(applications, port=0, host='',
        The files in this directory can be accessed via ``http://<host>:<port>/static/files``.
        The files in this directory can be accessed via ``http://<host>:<port>/static/files``.
        For example, if there is a ``A/B.jpg`` file in ``http_static_dir`` path,
        For example, if there is a ``A/B.jpg`` file in ``http_static_dir`` path,
        it can be accessed via ``http://<host>:<port>/static/A/B.jpg``.
        it can be accessed via ``http://<host>:<port>/static/A/B.jpg``.
+    :param int reconnect_timeout: The client can reconnect to server within ``reconnect_timeout`` seconds after an unexpected disconnection.
+       If set to 0 (default), once the client disconnects, the server session will be closed.
     :param list allowed_origins: The allowed request source list. (The current server host is always allowed)
     :param list allowed_origins: The allowed request source list. (The current server host is always allowed)
        The source contains the protocol, domain name, and port part.
        The source contains the protocol, domain name, and port part.
        Can use Unix shell-style wildcards:
        Can use Unix shell-style wildcards:
@@ -276,7 +351,8 @@ def start_server(applications, port=0, host='',
 
 
     cdn = cdn_validation(cdn, 'warn')  # if CDN is not available, warn user and disable CDN
     cdn = cdn_validation(cdn, 'warn')  # if CDN is not available, warn user and disable CDN
 
 
-    handler = webio_handler(applications, cdn, allowed_origins=allowed_origins, check_origin=check_origin)
+    handler = webio_handler(applications, cdn, allowed_origins=allowed_origins, check_origin=check_origin,
+                            reconnect_timeout=reconnect_timeout)
     _, port = _setup_server(webio_handler=handler, port=port, host=host, static_dir=static_dir, **tornado_app_settings)
     _, port = _setup_server(webio_handler=handler, port=port, host=host, static_dir=static_dir, **tornado_app_settings)
 
 
     print('Listen on %s:%s' % (host or '0.0.0.0', port))
     print('Listen on %s:%s' % (host or '0.0.0.0', port))
@@ -302,18 +378,26 @@ def start_server_in_current_thread_session():
 
 
         def open(self):
         def open(self):
             self.main_session = False
             self.main_session = False
+            cls = type(self)
             if SingleSessionWSHandler.session is None:
             if SingleSessionWSHandler.session is None:
                 self.main_session = True
                 self.main_session = True
                 SingleSessionWSHandler.instance = self
                 SingleSessionWSHandler.instance = self
+                self.session_id = 'main'
+                cls._connections[self.session_id] = self
+
                 session_info = get_session_info_from_headers(self.request.headers)
                 session_info = get_session_info_from_headers(self.request.headers)
                 session_info['user_ip'] = self.request.remote_ip
                 session_info['user_ip'] = self.request.remote_ip
                 session_info['request'] = self.request
                 session_info['request'] = self.request
                 session_info['backend'] = 'tornado'
                 session_info['backend'] = 'tornado'
                 session_info['protocol'] = 'websocket'
                 session_info['protocol'] = 'websocket'
-                SingleSessionWSHandler.session = ScriptModeSession(thread, session_info=session_info,
-                                                                   on_task_command=self.send_msg_to_client,
-                                                                   loop=asyncio.get_event_loop())
+                self.session = SingleSessionWSHandler.session = ScriptModeSession(
+                    thread, session_info=session_info,
+                    on_task_command=partial(self.send_msg_to_client, session_id=self.session_id),
+                    loop=asyncio.get_event_loop())
                 websocket_conn_opened.set()
                 websocket_conn_opened.set()
+
+                cls._webio_sessions[self.session_id] = self.session
+
             else:
             else:
                 self.close()
                 self.close()
 
 

+ 6 - 3
webiojs/src/handlers/base.ts

@@ -7,14 +7,17 @@ export interface CommandHandler {
     handle_message(msg: Command): void
     handle_message(msg: Command): void
 }
 }
 
 
-export class CloseHandler implements CommandHandler {
-    accept_command: string[] = ['close_session'];
+export class SessionCtrlHandler implements CommandHandler {
+    accept_command: string[] = ['close_session', 'set_session_id'];
 
 
     constructor(readonly session: Session) {
     constructor(readonly session: Session) {
     }
     }
 
 
     handle_message(msg: Command) {
     handle_message(msg: Command) {
-        this.session.close_session();
+        if (msg.command == 'close_session')
+            this.session.close_session();
+        else if (msg.command == 'set_session_id')
+            this.session.webio_session_id = msg.spec;
     }
     }
 }
 }
 
 

+ 10 - 5
webiojs/src/main.ts

@@ -2,7 +2,7 @@ import {config as appConfig, state} from "./state";
 import {ClientEvent, Command, HttpSession, is_http_backend, pushData, Session, WebSocketSession} from "./session";
 import {ClientEvent, Command, HttpSession, is_http_backend, pushData, Session, WebSocketSession} from "./session";
 import {InputHandler} from "./handlers/input"
 import {InputHandler} from "./handlers/input"
 import {OutputHandler} from "./handlers/output"
 import {OutputHandler} from "./handlers/output"
-import {CloseHandler, CommandDispatcher} from "./handlers/base"
+import {SessionCtrlHandler, CommandDispatcher} from "./handlers/base"
 import {PopupHandler} from "./handlers/popup";
 import {PopupHandler} from "./handlers/popup";
 import {openApp} from "./utils";
 import {openApp} from "./utils";
 import {ScriptHandler} from "./handlers/script";
 import {ScriptHandler} from "./handlers/script";
@@ -18,6 +18,10 @@ function backend_absaddr(addr: string) {
 // 初始化Handler和Session
 // 初始化Handler和Session
 function set_up_session(webio_session: Session, output_container_elem: JQuery, input_container_elem: JQuery) {
 function set_up_session(webio_session: Session, output_container_elem: JQuery, input_container_elem: JQuery) {
     state.CurrentSession = webio_session;
     state.CurrentSession = webio_session;
+    webio_session.on_session_create(function () {
+        $('#favicon32').attr('href', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAwklEQVQ4T63TvU5CQRCG4WcwMfEuqOgNtQ2Nd4CxV2LHtVhJ0N7AHdjQUBtrrLwLA4ks2Rx+/Qucw3Y78807M7sz4ft5dq6mI7RQX7o/JCNzfdfetkNifRk6k9wLN9jYdxMkyZPQ1faZXYUwB/OCix8V/W4Y4zJDCsBAX7jdM7iQJY+udELu+cTrP2X/xU2+NMPAg3B3UPaVOOmFoQkapQC8Z8AUpyUBs6MAKrZQ+RErf2PlQTrKKK8gpZdpewgOXOcFTTxEjYwMoIkAAAAASUVORK5CYII=');
+        $('#favicon16').attr('href', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABmUlEQVRYR82XK0wDQRCGv21TUUUJGBwGDBggGCSGBIcAWnBAgsNAgkKhSMDgCA8HtEXgSDBIDC9DDRgcpoSiKo52yea49DiutMttsz27M/98N7s7OyNo9tujgxSTwDiCIaAXSH27l4AXJA/AFSUuWOajGWnR0ChLP3HWkWSAZEN716CM4JQKW6R5+sunPkCeJJJNBCtAosnAQTMHyS6CDWYoh2mEAxzTR4JzYOCfgYNuBRymmOc5uPAbIMswMS6BbkPBPZkiVSZIc+/X/Qng/vl1C4LXIBzG/JmoAag9hxuDaa+XwAIw6p2JGkCObQSrhtMeLifZYZY1tegCqKsW4zHCadfldqgyqK6oC3DGIZIFXZVI9oIjplkUqArXyatGkYkU1+dc5p0eQY4MghNTqlo6kjkFsI9gScvRlLHkQJDnFhgxpampc6cAikCXpqMp8zcF8AnETSlq6lTaAsD6Flg+hNavofVCZL0UW3+M2uI5VhBWGxIFYL0lUxBWm1KviFttyz0Iq4OJB2F1NPO/qdaG0+DD3qLx/AuMVJFhmC8dSgAAAABJRU5ErkJggg==');
+    })
     webio_session.on_session_close(function () {
     webio_session.on_session_close(function () {
         $('#favicon32').attr('href', 'data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAByElEQVRYR82XLUzDUBDH/9emYoouYHAYMGCAYJAYEhxiW2EOSOYwkKBQKBIwuIUPN2g7gSPBIDF8GWbA4DAjG2qitEfesi6lbGxlXd5q393/fr333t07QpdfPp8f0nV9CcACEU0DGAOgN9yrAN6Y+QnATbVavcrlcp/dSFMnI9M0J1RV3WHmFQCJTvaN9RoRXbiuu28YxstfPm0BbNtOMPMeEW0C0LoMHDZzmPmIiHbT6XStlUZLgEKhMK5p2iWAyX8GDruVHMdZzmazr+GFXwCmac4oinINYCSm4L5M2fO8RcMwHoO6PwAaf37bh+BNCMdx5oOZaAKIPQdwF2Pa2yWwBGDOPxNNAMuyDohoK+a0t5Rj5sNMJrMtFusA4qopivLcw2mPyu14njclrmgdoFgsnjLzWlSVXuyJ6CyVSq2TqHDJZPI9QpHpJW7Qt1apVEbJsqwVIjqPSzWKDjOvCoBjItqI4hiXLTOfkG3b9wBm4xKNqPMgAMoAhiM6xmX+IQC+AKhxKUbUcQcCQPoWyD2E0q+h9EIkvRRLb0YD0Y4FhNQHiQCQ/iQTEFIfpX4Nl/os9yGkDiY+hNTRLNhSpQ2n4b7er/H8G7N6BRSbHvW5AAAAAElFTkSuQmCC');
         $('#favicon32').attr('href', 'data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAByElEQVRYR82XLUzDUBDH/9emYoouYHAYMGCAYJAYEhxiW2EOSOYwkKBQKBIwuIUPN2g7gSPBIDF8GWbA4DAjG2qitEfesi6lbGxlXd5q393/fr333t07QpdfPp8f0nV9CcACEU0DGAOgN9yrAN6Y+QnATbVavcrlcp/dSFMnI9M0J1RV3WHmFQCJTvaN9RoRXbiuu28YxstfPm0BbNtOMPMeEW0C0LoMHDZzmPmIiHbT6XStlUZLgEKhMK5p2iWAyX8GDruVHMdZzmazr+GFXwCmac4oinINYCSm4L5M2fO8RcMwHoO6PwAaf37bh+BNCMdx5oOZaAKIPQdwF2Pa2yWwBGDOPxNNAMuyDohoK+a0t5Rj5sNMJrMtFusA4qopivLcw2mPyu14njclrmgdoFgsnjLzWlSVXuyJ6CyVSq2TqHDJZPI9QpHpJW7Qt1apVEbJsqwVIjqPSzWKDjOvCoBjItqI4hiXLTOfkG3b9wBm4xKNqPMgAMoAhiM6xmX+IQC+AKhxKUbUcQcCQPoWyD2E0q+h9EIkvRRLb0YD0Y4FhNQHiQCQ/iQTEFIfpX4Nl/os9yGkDiY+hNTRLNhSpQ2n4b7er/H8G7N6BRSbHvW5AAAAAElFTkSuQmCC');
         $('#favicon16').attr('href', 'data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA0ElEQVQ4T62TPQrCQBCF30tA8BZW9mJtY+MNEtKr2HkWK0Xtw+4NbGysxVorbyEKyZMNRiSgmJ/tZufNNzO7M0ThxHHc8zxvSnIIoPNyXyXt0zRdR1F0+gxhblhr25IWJMcA3vcFviRtSc6DILg5XyZ0wQB2AAbFir7YBwAjB8kAxpg1ycmfwZlM0iYMwyldz77vH3+U/Y2rJEn6NMYsSc7KZM+1kla01p4BdKsAAFwc4A6gVRHwaARQr4Xaj1j7G2sPUiOjnEMqL9PnDJRd5ycpJXsd2f2NIAAAAABJRU5ErkJggg==');
         $('#favicon16').attr('href', 'data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA0ElEQVQ4T62TPQrCQBCF30tA8BZW9mJtY+MNEtKr2HkWK0Xtw+4NbGysxVorbyEKyZMNRiSgmJ/tZufNNzO7M0ThxHHc8zxvSnIIoPNyXyXt0zRdR1F0+gxhblhr25IWJMcA3vcFviRtSc6DILg5XyZ0wQB2AAbFir7YBwAjB8kAxpg1ycmfwZlM0iYMwyldz77vH3+U/Y2rJEn6NMYsSc7KZM+1kla01p4BdKsAAFwc4A6gVRHwaARQr4Xaj1j7G2sPUiOjnEMqL9PnDJRd5ycpJXsd2f2NIAAAAABJRU5ErkJggg==');
@@ -26,13 +30,13 @@ function set_up_session(webio_session: Session, output_container_elem: JQuery, i
     let output_ctrl = new OutputHandler(webio_session, output_container_elem);
     let output_ctrl = new OutputHandler(webio_session, output_container_elem);
     let input_ctrl = new InputHandler(webio_session, input_container_elem);
     let input_ctrl = new InputHandler(webio_session, input_container_elem);
     let popup_ctrl = new PopupHandler(webio_session);
     let popup_ctrl = new PopupHandler(webio_session);
-    let close_ctrl = new CloseHandler(webio_session);
+    let session_ctrl = new SessionCtrlHandler(webio_session);
     let script_ctrl = new ScriptHandler(webio_session);
     let script_ctrl = new ScriptHandler(webio_session);
     let download_ctrl = new DownloadHandler();
     let download_ctrl = new DownloadHandler();
     let toast_ctrl = new ToastHandler();
     let toast_ctrl = new ToastHandler();
     let env_ctrl = new EnvSettingHandler();
     let env_ctrl = new EnvSettingHandler();
 
 
-    let dispatcher = new CommandDispatcher(output_ctrl, input_ctrl, popup_ctrl, close_ctrl, script_ctrl, download_ctrl, toast_ctrl, env_ctrl);
+    let dispatcher = new CommandDispatcher(output_ctrl, input_ctrl, popup_ctrl, session_ctrl, script_ctrl, download_ctrl, toast_ctrl, env_ctrl);
 
 
     webio_session.on_server_message((msg: Command) => {
     webio_session.on_server_message((msg: Command) => {
         try {
         try {
@@ -59,7 +63,7 @@ function startWebIOClient(options: {
     }
     }
     const backend_addr = backend_absaddr(options.backend_address);
     const backend_addr = backend_absaddr(options.backend_address);
 
 
-    let start_session = (is_http:boolean) => {
+    let start_session = (is_http: boolean) => {
         let session;
         let session;
         if (is_http)
         if (is_http)
             session = new HttpSession(backend_addr, options.app_name, appConfig.httpPullInterval);
             session = new HttpSession(backend_addr, options.app_name, appConfig.httpPullInterval);
@@ -68,7 +72,7 @@ function startWebIOClient(options: {
         set_up_session(session, options.output_container_elem, options.input_container_elem);
         set_up_session(session, options.output_container_elem, options.input_container_elem);
         session.start_session(appConfig.debug);
         session.start_session(appConfig.debug);
     };
     };
-    if(options.protocol=='auto')
+    if (options.protocol == 'auto')
         is_http_backend(backend_addr).then(start_session);
         is_http_backend(backend_addr).then(start_session);
     else
     else
         start_session(options.protocol == 'http')
         start_session(options.protocol == 'http')
@@ -77,6 +81,7 @@ function startWebIOClient(options: {
 
 
 // @ts-ignore
 // @ts-ignore
 window.WebIO = {
 window.WebIO = {
+    '_state': state,
     'startWebIOClient': startWebIOClient,
     'startWebIOClient': startWebIOClient,
     'sendMessage': (msg: ClientEvent) => {
     'sendMessage': (msg: ClientEvent) => {
         return state.CurrentSession.send_message(msg);
         return state.CurrentSession.send_message(msg);

+ 34 - 10
webiojs/src/session.ts

@@ -21,6 +21,8 @@ export interface ClientEvent {
 * 提供的函数:start_session、send_message、close_session
 * 提供的函数:start_session、send_message、close_session
 * */
 * */
 export interface Session {
 export interface Session {
+    webio_session_id: string;
+
     on_session_create(callback: () => void): void;
     on_session_create(callback: () => void): void;
 
 
     on_session_close(callback: () => void): void;
     on_session_close(callback: () => void): void;
@@ -39,7 +41,9 @@ export interface Session {
 export class WebSocketSession implements Session {
 export class WebSocketSession implements Session {
     ws: WebSocket;
     ws: WebSocket;
     debug: boolean;
     debug: boolean;
-    private _closed: boolean;
+    webio_session_id: string = 'NEW';
+    private _closed: boolean; // session logic closed (by `close_session` command)
+    private _session_create_ts = 0;
     private _on_session_create: (this: WebSocket, ev: Event) => any = () => {
     private _on_session_create: (this: WebSocket, ev: Event) => any = () => {
     };
     };
     private _on_session_close: (this: WebSocket, ev: CloseEvent) => any = () => {
     private _on_session_close: (this: WebSocket, ev: CloseEvent) => any = () => {
@@ -47,20 +51,20 @@ export class WebSocketSession implements Session {
     private _on_server_message: (msg: Command) => any = () => {
     private _on_server_message: (msg: Command) => any = () => {
     };
     };
 
 
-    constructor(public ws_api: string, app_name: string = 'index') {
+    constructor(public ws_api: string, public app_name: string = 'index') {
         this.ws = null;
         this.ws = null;
         this.debug = false;
         this.debug = false;
         this._closed = false;
         this._closed = false;
-
-        let url = new URL(ws_api);
+    }
+    set_ws_api(){
+        let url = new URL(this.ws_api);
         if (url.protocol !== 'wss:' && url.protocol !== 'ws:') {
         if (url.protocol !== 'wss:' && url.protocol !== 'ws:') {
             let protocol = url.protocol || window.location.protocol;
             let protocol = url.protocol || window.location.protocol;
             url.protocol = protocol.replace('https', 'wss').replace('http', 'ws');
             url.protocol = protocol.replace('https', 'wss').replace('http', 'ws');
         }
         }
-        url.search = "?app=" + app_name;
+        url.search = `?app=${this.app_name}&session=${this.webio_session_id}`;
         this.ws_api = url.href;
         this.ws_api = url.href;
     }
     }
-
     on_session_create(callback: () => any): void {
     on_session_create(callback: () => any): void {
         this._on_session_create = callback;
         this._on_session_create = callback;
     };
     };
@@ -74,11 +78,27 @@ export class WebSocketSession implements Session {
     }
     }
 
 
     start_session(debug: boolean = false): void {
     start_session(debug: boolean = false): void {
+        let that = this;
+
+        this.set_ws_api();
+
+        this._session_create_ts = Date.now();
         this.debug = debug;
         this.debug = debug;
         this.ws = new WebSocket(this.ws_api);
         this.ws = new WebSocket(this.ws_api);
         this.ws.onopen = this._on_session_create;
         this.ws.onopen = this._on_session_create;
-        this.ws.onclose = this._on_session_close;
-        let that = this;
+
+        this.ws.onclose = function (evt) {
+            that._on_session_close.apply(that, evt);
+            if (!that._closed && that.webio_session_id!='NEW') {  // not receive `close_session` command && enabled reconnection
+                const session_create_interval = 5000;
+                if (Date.now() - that._session_create_ts > session_create_interval)
+                    that.start_session(that.debug);
+                else
+                    setTimeout(() => {
+                        that.start_session(that.debug);
+                    }, session_create_interval - Date.now() + that._session_create_ts);
+            }
+        };
         this.ws.onmessage = function (evt) {
         this.ws.onmessage = function (evt) {
             let msg: Command = JSON.parse(evt.data);
             let msg: Command = JSON.parse(evt.data);
             if (debug) console.info('>>>', JSON.parse(evt.data));
             if (debug) console.info('>>>', JSON.parse(evt.data));
@@ -161,7 +181,9 @@ export class HttpSession implements Session {
     start_session(debug: boolean = false): void {
     start_session(debug: boolean = false): void {
         this.debug = debug;
         this.debug = debug;
         this.pull();
         this.pull();
-        this.interval_pull_id = setInterval(()=>{this.pull()},this.pull_interval_ms);
+        this.interval_pull_id = setInterval(() => {
+            this.pull()
+        }, this.pull_interval_ms);
     }
     }
 
 
     pull() {
     pull() {
@@ -236,7 +258,9 @@ export class HttpSession implements Session {
     change_pull_interval(new_interval: number): void {
     change_pull_interval(new_interval: number): void {
         clearInterval(this.interval_pull_id);
         clearInterval(this.interval_pull_id);
         this.pull_interval_ms = new_interval;
         this.pull_interval_ms = new_interval;
-        this.interval_pull_id = setInterval(()=>{this.pull()}, this.pull_interval_ms);
+        this.interval_pull_id = setInterval(() => {
+            this.pull()
+        }, this.pull_interval_ms);
     }
     }
 }
 }