session.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import {error_alert} from "./utils";
  2. import {state} from "./state";
  3. import {t} from "./i18n";
  4. export interface Command {
  5. command: string
  6. task_id: string
  7. spec: any
  8. }
  9. export interface ClientEvent {
  10. event: string,
  11. task_id: string,
  12. data: any
  13. }
  14. /*
  15. * 会话
  16. * 向外暴露的事件:on_session_create、on_session_close、on_server_message
  17. * 提供的函数:start_session、send_message、close_session
  18. * */
  19. export interface Session {
  20. webio_session_id: string;
  21. on_session_create(callback: () => void): void;
  22. on_session_close(callback: () => void): void;
  23. on_server_message(callback: (msg: Command) => void): void;
  24. start_session(debug: boolean): void;
  25. send_message(msg: ClientEvent, onprogress?: (loaded: number, total: number) => void): void;
  26. send_buffer(data: Blob, onprogress?: (loaded: number, total: number) => void): void;
  27. close_session(): void;
  28. closed(): boolean;
  29. }
  30. export class WebSocketSession implements Session {
  31. ws: WebSocket;
  32. debug: boolean;
  33. webio_session_id: string = 'NEW';
  34. private _closed: boolean; // session logic closed (by `close_session` command)
  35. private _session_create_ts = 0;
  36. private _on_session_create: (this: WebSocket, ev: Event) => any = () => {
  37. };
  38. private _on_session_close: (this: WebSocket, ev: CloseEvent) => any = () => {
  39. };
  40. private _on_server_message: (msg: Command) => any = () => {
  41. };
  42. constructor(public ws_api: string, public app_name: string = 'index') {
  43. this.ws = null;
  44. this.debug = false;
  45. this._closed = false;
  46. }
  47. set_ws_api() {
  48. let url = new URL(this.ws_api);
  49. if (url.protocol !== 'wss:' && url.protocol !== 'ws:') {
  50. let protocol = url.protocol || window.location.protocol;
  51. url.protocol = protocol.replace('https', 'wss').replace('http', 'ws');
  52. }
  53. url.search = `?app=${this.app_name}&session=${this.webio_session_id}`;
  54. this.ws_api = url.href;
  55. }
  56. on_session_create(callback: () => any): void {
  57. this._on_session_create = callback;
  58. };
  59. on_session_close(callback: () => any): void {
  60. this._on_session_close = callback;
  61. }
  62. on_server_message(callback: (msg: Command) => any): void {
  63. this._on_server_message = callback;
  64. }
  65. start_session(debug: boolean = false): void {
  66. let that = this;
  67. this.set_ws_api();
  68. this._session_create_ts = Date.now();
  69. this.debug = debug;
  70. this.ws = new WebSocket(this.ws_api);
  71. this.ws.onopen = this._on_session_create;
  72. this.ws.onclose = function (evt) {
  73. that._on_session_close.apply(that, evt);
  74. if (!that._closed && that.webio_session_id != 'NEW') { // not receive `close_session` command && enabled reconnection
  75. const session_create_interval = 5000;
  76. if (Date.now() - that._session_create_ts > session_create_interval)
  77. that.start_session(that.debug);
  78. else
  79. setTimeout(() => {
  80. that.start_session(that.debug);
  81. }, session_create_interval - Date.now() + that._session_create_ts);
  82. }
  83. };
  84. this.ws.onmessage = function (evt) {
  85. let msg: Command = JSON.parse(evt.data);
  86. if (debug) console.info('>>>', JSON.parse(evt.data));
  87. that._on_server_message(msg);
  88. };
  89. }
  90. start_onprogress(onprogress?: (loaded: number, total: number) => void): void {
  91. let total = this.ws.bufferedAmount;
  92. let onprogressID = setInterval(() => {
  93. let loaded = total - this.ws.bufferedAmount;
  94. onprogress(loaded, total);
  95. if (this.ws.bufferedAmount == 0)
  96. clearInterval(onprogressID);
  97. }, 200);
  98. }
  99. send_message(msg: ClientEvent, onprogress?: (loaded: number, total: number) => void): void {
  100. if (this.closed())
  101. return error_alert(t("disconnected_with_server"));
  102. if (this.ws === null)
  103. return console.error('WebSocketWebIOSession.ws is null when invoke WebSocketWebIOSession.send_message. ' +
  104. 'Please call WebSocketWebIOSession.start_session first');
  105. this.ws.send(JSON.stringify(msg));
  106. if (onprogress)
  107. this.start_onprogress(onprogress);
  108. if (this.debug) console.info('<<<', msg);
  109. }
  110. send_buffer(data: Blob, onprogress?: (loaded: number, total: number) => void): void {
  111. if (this.closed())
  112. return error_alert(t("disconnected_with_server"));
  113. if (this.ws === null)
  114. return console.error('WebSocketWebIOSession.ws is null when invoke WebSocketWebIOSession.send_message. ' +
  115. 'Please call WebSocketWebIOSession.start_session first');
  116. this.ws.send(data);
  117. if (onprogress)
  118. this.start_onprogress(onprogress);
  119. if (this.debug) console.info('<<< Blob data...');
  120. }
  121. close_session(): void {
  122. this._closed = true;
  123. this._on_session_close.call(this.ws, null);
  124. try {
  125. this.ws.close()
  126. } catch (e) {
  127. }
  128. }
  129. closed(): boolean {
  130. return this._closed || this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING;
  131. }
  132. }
  133. export class HttpSession implements Session {
  134. interval_pull_id: number = null;
  135. webio_session_id: string = 'NEW';
  136. debug = false;
  137. private _closed = false;
  138. private _on_session_create: () => void = () => {
  139. };
  140. private _on_session_close: () => void = () => {
  141. };
  142. private _on_server_message: (msg: Command) => void = () => {
  143. };
  144. constructor(public api_url: string, app_name = 'index', public pull_interval_ms = 1000) {
  145. let url = new URL(api_url, window.location.href);
  146. url.search = "?app=" + app_name;
  147. this.api_url = url.href;
  148. }
  149. on_session_create(callback: () => void): void {
  150. this._on_session_create = callback;
  151. }
  152. on_session_close(callback: () => void): void {
  153. this._on_session_close = callback;
  154. }
  155. on_server_message(callback: (msg: Command) => void): void {
  156. this._on_server_message = callback;
  157. }
  158. start_session(debug: boolean = false): void {
  159. this.debug = debug;
  160. this.pull();
  161. this.interval_pull_id = setInterval(() => {
  162. this.pull()
  163. }, this.pull_interval_ms);
  164. }
  165. pull() {
  166. let that = this;
  167. $.ajax({
  168. type: "GET",
  169. url: this.api_url,
  170. contentType: "application/json; charset=utf-8",
  171. dataType: "json",
  172. headers: {"webio-session-id": this.webio_session_id},
  173. success: function (data: Command[], textStatus: string, jqXHR: JQuery.jqXHR) {
  174. that._on_session_create();
  175. that._on_request_success(data, textStatus, jqXHR);
  176. },
  177. error: function () {
  178. console.error('Http pulling failed');
  179. }
  180. })
  181. }
  182. private _on_request_success(data: Command[], textStatus: string, jqXHR: JQuery.jqXHR) {
  183. let sid = jqXHR.getResponseHeader('webio-session-id');
  184. if (sid) this.webio_session_id = sid;
  185. for (let msg of data) {
  186. if (this.debug) console.info('>>>', msg);
  187. this._on_server_message(msg);
  188. }
  189. };
  190. send_message(msg: ClientEvent, onprogress?: (loaded: number, total: number) => void): void {
  191. if (this.debug) console.info('<<<', msg);
  192. this._send({
  193. data: JSON.stringify(msg),
  194. contentType: "application/json; charset=utf-8",
  195. }, onprogress);
  196. }
  197. send_buffer(data: Blob, onprogress?: (loaded: number, total: number) => void): void {
  198. if (this.debug) console.info('<<< Blob data...');
  199. this._send({
  200. data: data,
  201. cache: false,
  202. processData: false,
  203. contentType: 'application/octet-stream',
  204. }, onprogress);
  205. }
  206. _send(options: { [key: string]: any; }, onprogress?: (loaded: number, total: number) => void): void {
  207. if (this.closed())
  208. return error_alert(t("disconnected_with_server"));
  209. $.ajax({
  210. ...options,
  211. type: "POST",
  212. url: this.api_url,
  213. dataType: "json",
  214. headers: {"webio-session-id": this.webio_session_id},
  215. success: this._on_request_success.bind(this),
  216. xhr: function () {
  217. let xhr = new window.XMLHttpRequest();
  218. // Upload progress
  219. xhr.upload.addEventListener("progress", function (evt) {
  220. if (evt.lengthComputable && onprogress) {
  221. onprogress(evt.loaded, evt.total);
  222. }
  223. }, false);
  224. return xhr;
  225. },
  226. error: function () {
  227. console.error('Http push blob data failed');
  228. error_alert(t("connect_fail"));
  229. }
  230. });
  231. }
  232. close_session(): void {
  233. this._closed = true;
  234. this._on_session_close();
  235. clearInterval(this.interval_pull_id);
  236. }
  237. closed(): boolean {
  238. return this._closed;
  239. }
  240. change_pull_interval(new_interval: number): void {
  241. clearInterval(this.interval_pull_id);
  242. this.pull_interval_ms = new_interval;
  243. this.interval_pull_id = setInterval(() => {
  244. this.pull()
  245. }, this.pull_interval_ms);
  246. }
  247. }
  248. /*
  249. * Check given `backend_addr` is a http backend
  250. * Usage:
  251. * // `http_backend` is a boolean to present whether or not a http_backend the given `backend_addr` is
  252. * is_http_backend('http://localhost:8080/io').then(function(http_backend){ });
  253. * */
  254. export function is_http_backend(backend_addr: string) {
  255. let url = new URL(backend_addr);
  256. let protocol = url.protocol || window.location.protocol;
  257. url.protocol = protocol.replace('wss', 'https').replace('ws', 'http');
  258. backend_addr = url.href;
  259. return new Promise(function (resolve, reject) {
  260. $.get(backend_addr, {test: 1}, undefined, 'html').done(function (data: string) {
  261. resolve(data === 'ok');
  262. }).fail(function (e: JQuery.jqXHR) {
  263. resolve(false);
  264. });
  265. });
  266. }
  267. // 向服务端发送数据
  268. export function pushData(data: any, callback_id: string) {
  269. if (state.CurrentSession === null)
  270. return console.error("can't invoke PushData when WebIOController is not instantiated");
  271. state.CurrentSession.send_message({
  272. event: "callback",
  273. task_id: callback_id,
  274. data: data
  275. });
  276. }