Fossil SCM
Add a connection retry back-off delay to /chat when it cannot reach the server.
Commit
4443ef843fdd91eee6cd3e274750d0db07e5f6085f056bcdbaeceaceeca66536
Parent
67f8111ea34a616…
2 files changed
+1
-1
+174
-19
+1
-1
| --- src/fossil.dom.js | ||
| +++ src/fossil.dom.js | ||
| @@ -21,11 +21,11 @@ | ||
| 21 | 21 | remove: function(e){ |
| 22 | 22 | if(e.forEach){ |
| 23 | 23 | e.forEach( |
| 24 | 24 | (x)=>x.parentNode.removeChild(x) |
| 25 | 25 | ); |
| 26 | - }else{ | |
| 26 | + }else if(e.parentNode){ | |
| 27 | 27 | e.parentNode.removeChild(e); |
| 28 | 28 | } |
| 29 | 29 | return e; |
| 30 | 30 | }, |
| 31 | 31 | /** |
| 32 | 32 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -21,11 +21,11 @@ | |
| 21 | remove: function(e){ |
| 22 | if(e.forEach){ |
| 23 | e.forEach( |
| 24 | (x)=>x.parentNode.removeChild(x) |
| 25 | ); |
| 26 | }else{ |
| 27 | e.parentNode.removeChild(e); |
| 28 | } |
| 29 | return e; |
| 30 | }, |
| 31 | /** |
| 32 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -21,11 +21,11 @@ | |
| 21 | remove: function(e){ |
| 22 | if(e.forEach){ |
| 23 | e.forEach( |
| 24 | (x)=>x.parentNode.removeChild(x) |
| 25 | ); |
| 26 | }else if(e.parentNode){ |
| 27 | e.parentNode.removeChild(e); |
| 28 | } |
| 29 | return e; |
| 30 | }, |
| 31 | /** |
| 32 |
+174
-19
| --- src/fossil.page.chat.js | ||
| +++ src/fossil.page.chat.js | ||
| @@ -180,10 +180,49 @@ | ||
| 180 | 180 | activeUser: undefined, |
| 181 | 181 | match: function(uname){ |
| 182 | 182 | return this.activeUser===uname || !this.activeUser; |
| 183 | 183 | } |
| 184 | 184 | }, |
| 185 | + /** | |
| 186 | + The timer object is used to control connection throttling | |
| 187 | + when connection errors arrise. It starts off with a polling | |
| 188 | + delay of $initialDelay ms. If there's a connection error, | |
| 189 | + that gets bumped by some value for each subsequent error, up | |
| 190 | + to some max value. | |
| 191 | + | |
| 192 | + The timeing of resetting the delay when service returns is, | |
| 193 | + because of the long-poll connection and our lack of low-level | |
| 194 | + insight into the connection at this level, a bit wonky. | |
| 195 | + */ | |
| 196 | + timer:{ | |
| 197 | + tidPoller: undefined /* poller timer */, | |
| 198 | + $initialDelay: 1000 /* initial polling interval (ms) */, | |
| 199 | + currentDelay: 1000 /* current polling interval */, | |
| 200 | + maxDelay: 60000 /* max interval when backing off for | |
| 201 | + connection errors */, | |
| 202 | + minDelay: 5000 /* minimum delay time */, | |
| 203 | + tidReconnect: undefined /*timer id for reconnection determination*/, | |
| 204 | + randomInterval: function(factor){ | |
| 205 | + return Math.floor(Math.random() * factor); | |
| 206 | + }, | |
| 207 | + incrDelay: function(){ | |
| 208 | + if( this.maxDelay > this.currentDelay ){ | |
| 209 | + if(this.currentDelay < this.minDelay){ | |
| 210 | + this.currentDelay = this.minDelay + this.randomInterval(this.minDelay/2); | |
| 211 | + }else{ | |
| 212 | + this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay/2); | |
| 213 | + } | |
| 214 | + } | |
| 215 | + return this.currentDelay; | |
| 216 | + }, | |
| 217 | + resetDelay: function(){ | |
| 218 | + return this.currentDelay = this.$initialDelay; | |
| 219 | + }, | |
| 220 | + isDelayed: function(){ | |
| 221 | + return this.currentDelay > this.$initialDelay; | |
| 222 | + } | |
| 223 | + }, | |
| 185 | 224 | /** |
| 186 | 225 | Gets (no args) or sets (1 arg) the current input text field |
| 187 | 226 | value, taking into account single- vs multi-line input. The |
| 188 | 227 | getter returns a trim()'d string and the setter returns this |
| 189 | 228 | object. As a special case, if arguments[0] is a boolean |
| @@ -233,10 +272,11 @@ | ||
| 233 | 272 | */ |
| 234 | 273 | ajaxStart: function(){ |
| 235 | 274 | if(1===++this.ajaxInflight){ |
| 236 | 275 | this.enableAjaxComponents(false); |
| 237 | 276 | } |
| 277 | + setupConnectionReestablished(); | |
| 238 | 278 | }, |
| 239 | 279 | /* Must be called after any ajax-related call for which |
| 240 | 280 | ajaxStart() was called, regardless of success or failure. If |
| 241 | 281 | it was the last such call (as measured by calls to |
| 242 | 282 | ajaxStart() and ajaxEnd()), elements disabled by a prior call |
| @@ -606,11 +646,11 @@ | ||
| 606 | 646 | |
| 607 | 647 | /** |
| 608 | 648 | If animations are enabled, passes its arguments |
| 609 | 649 | to D.addClassBriefly(), else this is a no-op. |
| 610 | 650 | If cb is a function, it is called after the |
| 611 | - CSS class is removed. Returns this object; | |
| 651 | + CSS class is removed. Returns this object; | |
| 612 | 652 | */ |
| 613 | 653 | animate: function f(e,a,cb){ |
| 614 | 654 | if(!f.$disabled){ |
| 615 | 655 | D.addClassBriefly(e, a, 0, cb); |
| 616 | 656 | } |
| @@ -645,33 +685,59 @@ | ||
| 645 | 685 | cs.reportError = function(/*msg args*/){ |
| 646 | 686 | const args = argsToArray(arguments); |
| 647 | 687 | console.error("chat error:",args); |
| 648 | 688 | F.toast.error.apply(F.toast, args); |
| 649 | 689 | }; |
| 690 | + | |
| 691 | + let InternalMsgId = 0; | |
| 650 | 692 | /** |
| 651 | 693 | Reports an error in the form of a new message in the chat |
| 652 | 694 | feed. All arguments are appended to the message's content area |
| 653 | 695 | using fossil.dom.append(), so may be of any type supported by |
| 654 | 696 | that function. |
| 655 | 697 | */ |
| 656 | 698 | cs.reportErrorAsMessage = function f(/*msg args*/){ |
| 657 | - if(undefined === f.$msgid) f.$msgid=0; | |
| 658 | 699 | const args = argsToArray(arguments).map(function(v){ |
| 659 | 700 | return (v instanceof Error) ? v.message : v; |
| 660 | 701 | }); |
| 661 | - console.error("chat error:",args); | |
| 702 | + if(Chat.verboseErrors){ | |
| 703 | + console.error("chat error:",args); | |
| 704 | + } | |
| 662 | 705 | const d = new Date().toISOString(), |
| 663 | 706 | mw = new this.MessageWidget({ |
| 664 | 707 | isError: true, |
| 665 | - xfrom: null, | |
| 666 | - msgid: "error-"+(++f.$msgid), | |
| 708 | + xfrom: undefined, | |
| 709 | + msgid: "error-"+(++InternalMsgId), | |
| 710 | + mtime: d, | |
| 711 | + lmtime: d, | |
| 712 | + xmsg: args | |
| 713 | + }); | |
| 714 | + this.injectMessageElem(mw.e.body); | |
| 715 | + mw.scrollIntoView(); | |
| 716 | + return mw; | |
| 717 | + }; | |
| 718 | + | |
| 719 | + /** | |
| 720 | + For use by the connection poller to send a "connection | |
| 721 | + restored" message. | |
| 722 | + */ | |
| 723 | + cs.reportReconnection = function f(/*msg args*/){ | |
| 724 | + const args = argsToArray(arguments).map(function(v){ | |
| 725 | + return (v instanceof Error) ? v.message : v; | |
| 726 | + }); | |
| 727 | + const d = new Date().toISOString(), | |
| 728 | + mw = new this.MessageWidget({ | |
| 729 | + isError: false, | |
| 730 | + xfrom: undefined, | |
| 731 | + msgid: "reconnect-"+(++InternalMsgId), | |
| 667 | 732 | mtime: d, |
| 668 | 733 | lmtime: d, |
| 669 | 734 | xmsg: args |
| 670 | 735 | }); |
| 671 | 736 | this.injectMessageElem(mw.e.body); |
| 672 | 737 | mw.scrollIntoView(); |
| 738 | + return mw; | |
| 673 | 739 | }; |
| 674 | 740 | |
| 675 | 741 | cs.getMessageElemById = function(id){ |
| 676 | 742 | return qs('[data-msgid="'+id+'"]'); |
| 677 | 743 | }; |
| @@ -776,10 +842,11 @@ | ||
| 776 | 842 | const self = this; |
| 777 | 843 | F.fetch('chat-fetch-one',{ |
| 778 | 844 | urlParams:{ name: id, raw: true}, |
| 779 | 845 | responseType: 'json', |
| 780 | 846 | onload: function(msg){ |
| 847 | + reportConnectionReestablished(); | |
| 781 | 848 | content.$elems[1] = D.append(D.pre(),msg.xmsg); |
| 782 | 849 | content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/; |
| 783 | 850 | self.toggleTextMode(e); |
| 784 | 851 | }, |
| 785 | 852 | aftersend:function(){ |
| @@ -837,11 +904,14 @@ | ||
| 837 | 904 | if(!(e instanceof HTMLElement)) return; |
| 838 | 905 | if(this.userMayDelete(e)){ |
| 839 | 906 | this.ajaxStart(); |
| 840 | 907 | F.fetch("chat-delete/" + id, { |
| 841 | 908 | responseType: 'json', |
| 842 | - onload:(r)=>this.deleteMessageElem(r), | |
| 909 | + onload:(r)=>{ | |
| 910 | + reportConnectionReestablished(); | |
| 911 | + this.deleteMessageElem(r); | |
| 912 | + }, | |
| 843 | 913 | onerror:(err)=>this.reportErrorAsMessage(err) |
| 844 | 914 | }); |
| 845 | 915 | }else{ |
| 846 | 916 | this.deleteMessageElem(id); |
| 847 | 917 | } |
| @@ -1457,10 +1527,11 @@ | ||
| 1457 | 1527 | n: nFetch, |
| 1458 | 1528 | i: iFirst |
| 1459 | 1529 | }, |
| 1460 | 1530 | responseType: "json", |
| 1461 | 1531 | onload:function(jx){ |
| 1532 | + reportConnectionReestablished(); | |
| 1462 | 1533 | if( bDown ) jx.msgs.reverse(); |
| 1463 | 1534 | jx.msgs.forEach((m) => { |
| 1464 | 1535 | m.isSearchResult = true; |
| 1465 | 1536 | var mw = new Chat.MessageWidget(m); |
| 1466 | 1537 | if( bDown ){ |
| @@ -1624,10 +1695,51 @@ | ||
| 1624 | 1695 | const theMsg = findMessageWidgetParent(w); |
| 1625 | 1696 | if(theMsg) Chat.deleteMessageElem(theMsg); |
| 1626 | 1697 | })); |
| 1627 | 1698 | Chat.reportErrorAsMessage(w); |
| 1628 | 1699 | }; |
| 1700 | + | |
| 1701 | + const removeConnectionErrors = function() { | |
| 1702 | + D.remove(Chat.e.viewMessages.querySelectorAll( | |
| 1703 | + '.message-widget.error-connection')); | |
| 1704 | + }; | |
| 1705 | + | |
| 1706 | + /* Assume the connection has been established, reset | |
| 1707 | + the Chat.timer.tidReconnect, and alert the user | |
| 1708 | + that the outage appears to be over. */ | |
| 1709 | + const reportConnectionReestablished = function(){ | |
| 1710 | + if( Chat.timer.tidReconnect ){ | |
| 1711 | + clearTimeout(Chat.timer.tidReconnect); | |
| 1712 | + Chat.timer.tidReconnect = 0; | |
| 1713 | + } | |
| 1714 | + if( Chat.timer.isDelayed() ){ | |
| 1715 | + removeConnectionErrors(); | |
| 1716 | + Chat.timer.resetDelay(); | |
| 1717 | + Chat.reportReconnection( | |
| 1718 | + "Connection restored after outage." | |
| 1719 | + ); | |
| 1720 | + setTimeout( Chat.poll, 0 ); | |
| 1721 | + } | |
| 1722 | + }; | |
| 1723 | + | |
| 1724 | + /* To be called from F.fetch() beforesend() handlers. If we're | |
| 1725 | + currently in delayed-retry mode and a connection is start, try to | |
| 1726 | + reset the delay after N time waiting on that connection. The fact | |
| 1727 | + that the connection is waiting to respond, rather than outright | |
| 1728 | + failing, is a good hint that the outage is over and we can reset | |
| 1729 | + the back-off timer. */ | |
| 1730 | + const setupConnectionReestablished = function(){ | |
| 1731 | + if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){ | |
| 1732 | + Chat.timer.tidReconnect = setTimeout(()=>{ | |
| 1733 | + Chat.timer.tidReconnect = 0; | |
| 1734 | + if( poll.running ){ | |
| 1735 | + reportConnectionReestablished(); | |
| 1736 | + } | |
| 1737 | + }, Chat.timer.$initialDelay * 5 ); | |
| 1738 | + removeConnectionErrors(); | |
| 1739 | + } | |
| 1740 | + }; | |
| 1629 | 1741 | |
| 1630 | 1742 | /** |
| 1631 | 1743 | Submits the contents of the message input field (if not empty) |
| 1632 | 1744 | and/or the file attachment field to the server. If both are |
| 1633 | 1745 | empty, this is a no-op. |
| @@ -1686,10 +1798,11 @@ | ||
| 1686 | 1798 | onerror:function(err){ |
| 1687 | 1799 | self.reportErrorAsMessage(err); |
| 1688 | 1800 | recoverFailedMessage(fallback); |
| 1689 | 1801 | }, |
| 1690 | 1802 | onload:function(txt){ |
| 1803 | + reportConnectionReestablished(); | |
| 1691 | 1804 | if(!txt) return/*success response*/; |
| 1692 | 1805 | try{ |
| 1693 | 1806 | const json = JSON.parse(txt); |
| 1694 | 1807 | self.newContent({msgs:[json]}); |
| 1695 | 1808 | }catch(e){ |
| @@ -2185,10 +2298,11 @@ | ||
| 2185 | 2298 | /*filename needed for mimetype determination*/); |
| 2186 | 2299 | fd.append('render_mode',F.page.previewModes.wiki); |
| 2187 | 2300 | F.fetch('ajax/preview-text',{ |
| 2188 | 2301 | payload: fd, |
| 2189 | 2302 | onload: function(html){ |
| 2303 | + reportConnectionReestablished(); | |
| 2190 | 2304 | Chat.setPreviewText(html); |
| 2191 | 2305 | F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr')); |
| 2192 | 2306 | }, |
| 2193 | 2307 | onerror: function(e){ |
| 2194 | 2308 | F.fetch.onerror(e); |
| @@ -2322,10 +2436,11 @@ | ||
| 2322 | 2436 | onerror:function(err){ |
| 2323 | 2437 | Chat.reportErrorAsMessage(err); |
| 2324 | 2438 | Chat._isBatchLoading = false; |
| 2325 | 2439 | }, |
| 2326 | 2440 | onload:function(x){ |
| 2441 | + reportConnectionReestablished(); | |
| 2327 | 2442 | let gotMessages = x.msgs.length; |
| 2328 | 2443 | newcontent(x,true); |
| 2329 | 2444 | Chat._isBatchLoading = false; |
| 2330 | 2445 | Chat.updateActiveUserList(); |
| 2331 | 2446 | if(Chat._gotServerError){ |
| @@ -2411,10 +2526,11 @@ | ||
| 2411 | 2526 | onerror:function(err){ |
| 2412 | 2527 | Chat.setCurrentView(Chat.e.viewMessages); |
| 2413 | 2528 | Chat.reportErrorAsMessage(err); |
| 2414 | 2529 | }, |
| 2415 | 2530 | onload:function(jx){ |
| 2531 | + reportConnectionReestablished(); | |
| 2416 | 2532 | let previd = 0; |
| 2417 | 2533 | D.clearElement(eMsgTgt); |
| 2418 | 2534 | jx.msgs.forEach((m)=>{ |
| 2419 | 2535 | m.isSearchResult = true; |
| 2420 | 2536 | const mw = new Chat.MessageWidget(m); |
| @@ -2444,29 +2560,62 @@ | ||
| 2444 | 2560 | } |
| 2445 | 2561 | } |
| 2446 | 2562 | ); |
| 2447 | 2563 | }/*Chat.submitSearch()*/; |
| 2448 | 2564 | |
| 2449 | - const afterFetch = function f(){ | |
| 2565 | + /** | |
| 2566 | + Deal with the last poll() response and maybe re-start poll(). | |
| 2567 | + */ | |
| 2568 | + const afterPollFetch = function f(err){ | |
| 2450 | 2569 | if(true===f.isFirstCall){ |
| 2451 | 2570 | f.isFirstCall = false; |
| 2452 | 2571 | Chat.ajaxEnd(); |
| 2453 | 2572 | Chat.e.viewMessages.classList.remove('loading'); |
| 2454 | 2573 | setTimeout(function(){ |
| 2455 | 2574 | Chat.scrollMessagesTo(1); |
| 2456 | 2575 | }, 250); |
| 2457 | 2576 | } |
| 2458 | - if(Chat._gotServerError && Chat.intervalTimer){ | |
| 2459 | - clearInterval(Chat.intervalTimer); | |
| 2577 | + if(Chat.timer.tidPoller) { | |
| 2578 | + clearTimeout(Chat.timer.tidPoller); | |
| 2579 | + Chat.timer.tidPoller = 0; | |
| 2580 | + } | |
| 2581 | + if(Chat._gotServerError){ | |
| 2460 | 2582 | Chat.reportErrorAsMessage( |
| 2461 | 2583 | "Shutting down chat poller due to server-side error. ", |
| 2462 | - "Reload this page to reactivate it."); | |
| 2463 | - delete Chat.intervalTimer; | |
| 2584 | + "Reload this page to reactivate it." | |
| 2585 | + ); | |
| 2586 | + Chat.timer.tidPoller = undefined; | |
| 2587 | + poll.running = false; | |
| 2588 | + } else { | |
| 2589 | + poll.running = false; | |
| 2590 | + if( !err ){ | |
| 2591 | + /* Restart the poller. */ | |
| 2592 | + Chat.timer.tidPoller = setTimeout( | |
| 2593 | + poll, Chat.timer.resetDelay() | |
| 2594 | + ); | |
| 2595 | + }else{ | |
| 2596 | + /* Delay a while before trying again, noting that other Chat | |
| 2597 | + APIs may try and succeed at connections before this timer | |
| 2598 | + resolves. */ | |
| 2599 | + const delay = Chat.timer.incrDelay(); | |
| 2600 | + const msg = D.addClass( | |
| 2601 | + Chat.reportErrorAsMessage( | |
| 2602 | + "Connection error. Retrying in ", | |
| 2603 | + delay, " ms." | |
| 2604 | + ).e.body, | |
| 2605 | + 'error-connection' | |
| 2606 | + ); | |
| 2607 | + Chat.timer.tidPoller = setTimeout(()=>{ | |
| 2608 | + D.remove(msg); | |
| 2609 | + poll(); | |
| 2610 | + }, delay); | |
| 2611 | + } | |
| 2612 | + //console.log("isOkay =",isOkay,"currentDelay =",Chat.timer.currentDelay); | |
| 2464 | 2613 | } |
| 2465 | - poll.running = false; | |
| 2466 | 2614 | }; |
| 2467 | - afterFetch.isFirstCall = true; | |
| 2615 | + afterPollFetch.isFirstCall = true; | |
| 2616 | + | |
| 2468 | 2617 | /** |
| 2469 | 2618 | FIXME: when polling fails because the remote server is |
| 2470 | 2619 | reachable but it's not accepting HTTP requests, we should back |
| 2471 | 2620 | off on polling for a while. e.g. if the remote web server process |
| 2472 | 2621 | is killed, the poll fails quickly and immediately retries, |
| @@ -2489,42 +2638,48 @@ | ||
| 2489 | 2638 | Chat.ajaxStart(); |
| 2490 | 2639 | Chat.e.viewMessages.classList.add('loading'); |
| 2491 | 2640 | } |
| 2492 | 2641 | F.fetch("chat-poll",{ |
| 2493 | 2642 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 2643 | + //timeout: 8000, | |
| 2494 | 2644 | urlParams:{ |
| 2495 | 2645 | name: Chat.mxMsg |
| 2496 | 2646 | }, |
| 2497 | 2647 | responseType: "json", |
| 2498 | 2648 | // Disable the ajax start/end handling for this long-polling op: |
| 2499 | - beforesend: function(){}, | |
| 2500 | - aftersend: function(){}, | |
| 2649 | + beforesend: function(){ | |
| 2650 | + setupConnectionReestablished(); | |
| 2651 | + }, | |
| 2652 | + aftersend: function(){ | |
| 2653 | + }, | |
| 2501 | 2654 | onerror:function(err){ |
| 2502 | 2655 | Chat._isBatchLoading = false; |
| 2503 | - if(Chat.verboseErrors) console.error(err); | |
| 2656 | + if(Chat.verboseErrors) console.error("poll onerror:",err); | |
| 2504 | 2657 | /* ^^^ we don't use Chat.reportError() here b/c the polling |
| 2505 | 2658 | fails exepectedly when it times out, but is then immediately |
| 2506 | 2659 | resumed, and reportError() produces a loud error message. */ |
| 2507 | - afterFetch(); | |
| 2660 | + afterPollFetch(err); | |
| 2508 | 2661 | }, |
| 2509 | 2662 | onload:function(y){ |
| 2663 | + reportConnectionReestablished(); | |
| 2510 | 2664 | newcontent(y); |
| 2511 | 2665 | if(Chat._isBatchLoading){ |
| 2512 | 2666 | Chat._isBatchLoading = false; |
| 2513 | 2667 | Chat.updateActiveUserList(); |
| 2514 | 2668 | } |
| 2515 | - afterFetch(); | |
| 2669 | + afterPollFetch(); | |
| 2516 | 2670 | } |
| 2517 | 2671 | }); |
| 2518 | 2672 | }; |
| 2519 | 2673 | poll.isFirstCall = true; |
| 2674 | + Chat.poll = poll; | |
| 2520 | 2675 | Chat._gotServerError = poll.running = false; |
| 2521 | 2676 | if( window.fossil.config.chat.fromcli ){ |
| 2522 | 2677 | Chat.chatOnlyMode(true); |
| 2523 | 2678 | } |
| 2524 | - Chat.intervalTimer = setInterval(poll, 1000); | |
| 2679 | + Chat.timer.tidPoller = setTimeout(poll, Chat.timer.resetDelay()); | |
| 2525 | 2680 | delete ForceResizeKludge.$disabled; |
| 2526 | 2681 | ForceResizeKludge(); |
| 2527 | 2682 | Chat.animate.$disabled = false; |
| 2528 | 2683 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2529 | 2684 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2530 | 2685 | }); |
| 2531 | 2686 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -180,10 +180,49 @@ | |
| 180 | activeUser: undefined, |
| 181 | match: function(uname){ |
| 182 | return this.activeUser===uname || !this.activeUser; |
| 183 | } |
| 184 | }, |
| 185 | /** |
| 186 | Gets (no args) or sets (1 arg) the current input text field |
| 187 | value, taking into account single- vs multi-line input. The |
| 188 | getter returns a trim()'d string and the setter returns this |
| 189 | object. As a special case, if arguments[0] is a boolean |
| @@ -233,10 +272,11 @@ | |
| 233 | */ |
| 234 | ajaxStart: function(){ |
| 235 | if(1===++this.ajaxInflight){ |
| 236 | this.enableAjaxComponents(false); |
| 237 | } |
| 238 | }, |
| 239 | /* Must be called after any ajax-related call for which |
| 240 | ajaxStart() was called, regardless of success or failure. If |
| 241 | it was the last such call (as measured by calls to |
| 242 | ajaxStart() and ajaxEnd()), elements disabled by a prior call |
| @@ -606,11 +646,11 @@ | |
| 606 | |
| 607 | /** |
| 608 | If animations are enabled, passes its arguments |
| 609 | to D.addClassBriefly(), else this is a no-op. |
| 610 | If cb is a function, it is called after the |
| 611 | CSS class is removed. Returns this object; |
| 612 | */ |
| 613 | animate: function f(e,a,cb){ |
| 614 | if(!f.$disabled){ |
| 615 | D.addClassBriefly(e, a, 0, cb); |
| 616 | } |
| @@ -645,33 +685,59 @@ | |
| 645 | cs.reportError = function(/*msg args*/){ |
| 646 | const args = argsToArray(arguments); |
| 647 | console.error("chat error:",args); |
| 648 | F.toast.error.apply(F.toast, args); |
| 649 | }; |
| 650 | /** |
| 651 | Reports an error in the form of a new message in the chat |
| 652 | feed. All arguments are appended to the message's content area |
| 653 | using fossil.dom.append(), so may be of any type supported by |
| 654 | that function. |
| 655 | */ |
| 656 | cs.reportErrorAsMessage = function f(/*msg args*/){ |
| 657 | if(undefined === f.$msgid) f.$msgid=0; |
| 658 | const args = argsToArray(arguments).map(function(v){ |
| 659 | return (v instanceof Error) ? v.message : v; |
| 660 | }); |
| 661 | console.error("chat error:",args); |
| 662 | const d = new Date().toISOString(), |
| 663 | mw = new this.MessageWidget({ |
| 664 | isError: true, |
| 665 | xfrom: null, |
| 666 | msgid: "error-"+(++f.$msgid), |
| 667 | mtime: d, |
| 668 | lmtime: d, |
| 669 | xmsg: args |
| 670 | }); |
| 671 | this.injectMessageElem(mw.e.body); |
| 672 | mw.scrollIntoView(); |
| 673 | }; |
| 674 | |
| 675 | cs.getMessageElemById = function(id){ |
| 676 | return qs('[data-msgid="'+id+'"]'); |
| 677 | }; |
| @@ -776,10 +842,11 @@ | |
| 776 | const self = this; |
| 777 | F.fetch('chat-fetch-one',{ |
| 778 | urlParams:{ name: id, raw: true}, |
| 779 | responseType: 'json', |
| 780 | onload: function(msg){ |
| 781 | content.$elems[1] = D.append(D.pre(),msg.xmsg); |
| 782 | content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/; |
| 783 | self.toggleTextMode(e); |
| 784 | }, |
| 785 | aftersend:function(){ |
| @@ -837,11 +904,14 @@ | |
| 837 | if(!(e instanceof HTMLElement)) return; |
| 838 | if(this.userMayDelete(e)){ |
| 839 | this.ajaxStart(); |
| 840 | F.fetch("chat-delete/" + id, { |
| 841 | responseType: 'json', |
| 842 | onload:(r)=>this.deleteMessageElem(r), |
| 843 | onerror:(err)=>this.reportErrorAsMessage(err) |
| 844 | }); |
| 845 | }else{ |
| 846 | this.deleteMessageElem(id); |
| 847 | } |
| @@ -1457,10 +1527,11 @@ | |
| 1457 | n: nFetch, |
| 1458 | i: iFirst |
| 1459 | }, |
| 1460 | responseType: "json", |
| 1461 | onload:function(jx){ |
| 1462 | if( bDown ) jx.msgs.reverse(); |
| 1463 | jx.msgs.forEach((m) => { |
| 1464 | m.isSearchResult = true; |
| 1465 | var mw = new Chat.MessageWidget(m); |
| 1466 | if( bDown ){ |
| @@ -1624,10 +1695,51 @@ | |
| 1624 | const theMsg = findMessageWidgetParent(w); |
| 1625 | if(theMsg) Chat.deleteMessageElem(theMsg); |
| 1626 | })); |
| 1627 | Chat.reportErrorAsMessage(w); |
| 1628 | }; |
| 1629 | |
| 1630 | /** |
| 1631 | Submits the contents of the message input field (if not empty) |
| 1632 | and/or the file attachment field to the server. If both are |
| 1633 | empty, this is a no-op. |
| @@ -1686,10 +1798,11 @@ | |
| 1686 | onerror:function(err){ |
| 1687 | self.reportErrorAsMessage(err); |
| 1688 | recoverFailedMessage(fallback); |
| 1689 | }, |
| 1690 | onload:function(txt){ |
| 1691 | if(!txt) return/*success response*/; |
| 1692 | try{ |
| 1693 | const json = JSON.parse(txt); |
| 1694 | self.newContent({msgs:[json]}); |
| 1695 | }catch(e){ |
| @@ -2185,10 +2298,11 @@ | |
| 2185 | /*filename needed for mimetype determination*/); |
| 2186 | fd.append('render_mode',F.page.previewModes.wiki); |
| 2187 | F.fetch('ajax/preview-text',{ |
| 2188 | payload: fd, |
| 2189 | onload: function(html){ |
| 2190 | Chat.setPreviewText(html); |
| 2191 | F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr')); |
| 2192 | }, |
| 2193 | onerror: function(e){ |
| 2194 | F.fetch.onerror(e); |
| @@ -2322,10 +2436,11 @@ | |
| 2322 | onerror:function(err){ |
| 2323 | Chat.reportErrorAsMessage(err); |
| 2324 | Chat._isBatchLoading = false; |
| 2325 | }, |
| 2326 | onload:function(x){ |
| 2327 | let gotMessages = x.msgs.length; |
| 2328 | newcontent(x,true); |
| 2329 | Chat._isBatchLoading = false; |
| 2330 | Chat.updateActiveUserList(); |
| 2331 | if(Chat._gotServerError){ |
| @@ -2411,10 +2526,11 @@ | |
| 2411 | onerror:function(err){ |
| 2412 | Chat.setCurrentView(Chat.e.viewMessages); |
| 2413 | Chat.reportErrorAsMessage(err); |
| 2414 | }, |
| 2415 | onload:function(jx){ |
| 2416 | let previd = 0; |
| 2417 | D.clearElement(eMsgTgt); |
| 2418 | jx.msgs.forEach((m)=>{ |
| 2419 | m.isSearchResult = true; |
| 2420 | const mw = new Chat.MessageWidget(m); |
| @@ -2444,29 +2560,62 @@ | |
| 2444 | } |
| 2445 | } |
| 2446 | ); |
| 2447 | }/*Chat.submitSearch()*/; |
| 2448 | |
| 2449 | const afterFetch = function f(){ |
| 2450 | if(true===f.isFirstCall){ |
| 2451 | f.isFirstCall = false; |
| 2452 | Chat.ajaxEnd(); |
| 2453 | Chat.e.viewMessages.classList.remove('loading'); |
| 2454 | setTimeout(function(){ |
| 2455 | Chat.scrollMessagesTo(1); |
| 2456 | }, 250); |
| 2457 | } |
| 2458 | if(Chat._gotServerError && Chat.intervalTimer){ |
| 2459 | clearInterval(Chat.intervalTimer); |
| 2460 | Chat.reportErrorAsMessage( |
| 2461 | "Shutting down chat poller due to server-side error. ", |
| 2462 | "Reload this page to reactivate it."); |
| 2463 | delete Chat.intervalTimer; |
| 2464 | } |
| 2465 | poll.running = false; |
| 2466 | }; |
| 2467 | afterFetch.isFirstCall = true; |
| 2468 | /** |
| 2469 | FIXME: when polling fails because the remote server is |
| 2470 | reachable but it's not accepting HTTP requests, we should back |
| 2471 | off on polling for a while. e.g. if the remote web server process |
| 2472 | is killed, the poll fails quickly and immediately retries, |
| @@ -2489,42 +2638,48 @@ | |
| 2489 | Chat.ajaxStart(); |
| 2490 | Chat.e.viewMessages.classList.add('loading'); |
| 2491 | } |
| 2492 | F.fetch("chat-poll",{ |
| 2493 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 2494 | urlParams:{ |
| 2495 | name: Chat.mxMsg |
| 2496 | }, |
| 2497 | responseType: "json", |
| 2498 | // Disable the ajax start/end handling for this long-polling op: |
| 2499 | beforesend: function(){}, |
| 2500 | aftersend: function(){}, |
| 2501 | onerror:function(err){ |
| 2502 | Chat._isBatchLoading = false; |
| 2503 | if(Chat.verboseErrors) console.error(err); |
| 2504 | /* ^^^ we don't use Chat.reportError() here b/c the polling |
| 2505 | fails exepectedly when it times out, but is then immediately |
| 2506 | resumed, and reportError() produces a loud error message. */ |
| 2507 | afterFetch(); |
| 2508 | }, |
| 2509 | onload:function(y){ |
| 2510 | newcontent(y); |
| 2511 | if(Chat._isBatchLoading){ |
| 2512 | Chat._isBatchLoading = false; |
| 2513 | Chat.updateActiveUserList(); |
| 2514 | } |
| 2515 | afterFetch(); |
| 2516 | } |
| 2517 | }); |
| 2518 | }; |
| 2519 | poll.isFirstCall = true; |
| 2520 | Chat._gotServerError = poll.running = false; |
| 2521 | if( window.fossil.config.chat.fromcli ){ |
| 2522 | Chat.chatOnlyMode(true); |
| 2523 | } |
| 2524 | Chat.intervalTimer = setInterval(poll, 1000); |
| 2525 | delete ForceResizeKludge.$disabled; |
| 2526 | ForceResizeKludge(); |
| 2527 | Chat.animate.$disabled = false; |
| 2528 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2529 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2530 | }); |
| 2531 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -180,10 +180,49 @@ | |
| 180 | activeUser: undefined, |
| 181 | match: function(uname){ |
| 182 | return this.activeUser===uname || !this.activeUser; |
| 183 | } |
| 184 | }, |
| 185 | /** |
| 186 | The timer object is used to control connection throttling |
| 187 | when connection errors arrise. It starts off with a polling |
| 188 | delay of $initialDelay ms. If there's a connection error, |
| 189 | that gets bumped by some value for each subsequent error, up |
| 190 | to some max value. |
| 191 | |
| 192 | The timeing of resetting the delay when service returns is, |
| 193 | because of the long-poll connection and our lack of low-level |
| 194 | insight into the connection at this level, a bit wonky. |
| 195 | */ |
| 196 | timer:{ |
| 197 | tidPoller: undefined /* poller timer */, |
| 198 | $initialDelay: 1000 /* initial polling interval (ms) */, |
| 199 | currentDelay: 1000 /* current polling interval */, |
| 200 | maxDelay: 60000 /* max interval when backing off for |
| 201 | connection errors */, |
| 202 | minDelay: 5000 /* minimum delay time */, |
| 203 | tidReconnect: undefined /*timer id for reconnection determination*/, |
| 204 | randomInterval: function(factor){ |
| 205 | return Math.floor(Math.random() * factor); |
| 206 | }, |
| 207 | incrDelay: function(){ |
| 208 | if( this.maxDelay > this.currentDelay ){ |
| 209 | if(this.currentDelay < this.minDelay){ |
| 210 | this.currentDelay = this.minDelay + this.randomInterval(this.minDelay/2); |
| 211 | }else{ |
| 212 | this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay/2); |
| 213 | } |
| 214 | } |
| 215 | return this.currentDelay; |
| 216 | }, |
| 217 | resetDelay: function(){ |
| 218 | return this.currentDelay = this.$initialDelay; |
| 219 | }, |
| 220 | isDelayed: function(){ |
| 221 | return this.currentDelay > this.$initialDelay; |
| 222 | } |
| 223 | }, |
| 224 | /** |
| 225 | Gets (no args) or sets (1 arg) the current input text field |
| 226 | value, taking into account single- vs multi-line input. The |
| 227 | getter returns a trim()'d string and the setter returns this |
| 228 | object. As a special case, if arguments[0] is a boolean |
| @@ -233,10 +272,11 @@ | |
| 272 | */ |
| 273 | ajaxStart: function(){ |
| 274 | if(1===++this.ajaxInflight){ |
| 275 | this.enableAjaxComponents(false); |
| 276 | } |
| 277 | setupConnectionReestablished(); |
| 278 | }, |
| 279 | /* Must be called after any ajax-related call for which |
| 280 | ajaxStart() was called, regardless of success or failure. If |
| 281 | it was the last such call (as measured by calls to |
| 282 | ajaxStart() and ajaxEnd()), elements disabled by a prior call |
| @@ -606,11 +646,11 @@ | |
| 646 | |
| 647 | /** |
| 648 | If animations are enabled, passes its arguments |
| 649 | to D.addClassBriefly(), else this is a no-op. |
| 650 | If cb is a function, it is called after the |
| 651 | CSS class is removed. Returns this object; |
| 652 | */ |
| 653 | animate: function f(e,a,cb){ |
| 654 | if(!f.$disabled){ |
| 655 | D.addClassBriefly(e, a, 0, cb); |
| 656 | } |
| @@ -645,33 +685,59 @@ | |
| 685 | cs.reportError = function(/*msg args*/){ |
| 686 | const args = argsToArray(arguments); |
| 687 | console.error("chat error:",args); |
| 688 | F.toast.error.apply(F.toast, args); |
| 689 | }; |
| 690 | |
| 691 | let InternalMsgId = 0; |
| 692 | /** |
| 693 | Reports an error in the form of a new message in the chat |
| 694 | feed. All arguments are appended to the message's content area |
| 695 | using fossil.dom.append(), so may be of any type supported by |
| 696 | that function. |
| 697 | */ |
| 698 | cs.reportErrorAsMessage = function f(/*msg args*/){ |
| 699 | const args = argsToArray(arguments).map(function(v){ |
| 700 | return (v instanceof Error) ? v.message : v; |
| 701 | }); |
| 702 | if(Chat.verboseErrors){ |
| 703 | console.error("chat error:",args); |
| 704 | } |
| 705 | const d = new Date().toISOString(), |
| 706 | mw = new this.MessageWidget({ |
| 707 | isError: true, |
| 708 | xfrom: undefined, |
| 709 | msgid: "error-"+(++InternalMsgId), |
| 710 | mtime: d, |
| 711 | lmtime: d, |
| 712 | xmsg: args |
| 713 | }); |
| 714 | this.injectMessageElem(mw.e.body); |
| 715 | mw.scrollIntoView(); |
| 716 | return mw; |
| 717 | }; |
| 718 | |
| 719 | /** |
| 720 | For use by the connection poller to send a "connection |
| 721 | restored" message. |
| 722 | */ |
| 723 | cs.reportReconnection = function f(/*msg args*/){ |
| 724 | const args = argsToArray(arguments).map(function(v){ |
| 725 | return (v instanceof Error) ? v.message : v; |
| 726 | }); |
| 727 | const d = new Date().toISOString(), |
| 728 | mw = new this.MessageWidget({ |
| 729 | isError: false, |
| 730 | xfrom: undefined, |
| 731 | msgid: "reconnect-"+(++InternalMsgId), |
| 732 | mtime: d, |
| 733 | lmtime: d, |
| 734 | xmsg: args |
| 735 | }); |
| 736 | this.injectMessageElem(mw.e.body); |
| 737 | mw.scrollIntoView(); |
| 738 | return mw; |
| 739 | }; |
| 740 | |
| 741 | cs.getMessageElemById = function(id){ |
| 742 | return qs('[data-msgid="'+id+'"]'); |
| 743 | }; |
| @@ -776,10 +842,11 @@ | |
| 842 | const self = this; |
| 843 | F.fetch('chat-fetch-one',{ |
| 844 | urlParams:{ name: id, raw: true}, |
| 845 | responseType: 'json', |
| 846 | onload: function(msg){ |
| 847 | reportConnectionReestablished(); |
| 848 | content.$elems[1] = D.append(D.pre(),msg.xmsg); |
| 849 | content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/; |
| 850 | self.toggleTextMode(e); |
| 851 | }, |
| 852 | aftersend:function(){ |
| @@ -837,11 +904,14 @@ | |
| 904 | if(!(e instanceof HTMLElement)) return; |
| 905 | if(this.userMayDelete(e)){ |
| 906 | this.ajaxStart(); |
| 907 | F.fetch("chat-delete/" + id, { |
| 908 | responseType: 'json', |
| 909 | onload:(r)=>{ |
| 910 | reportConnectionReestablished(); |
| 911 | this.deleteMessageElem(r); |
| 912 | }, |
| 913 | onerror:(err)=>this.reportErrorAsMessage(err) |
| 914 | }); |
| 915 | }else{ |
| 916 | this.deleteMessageElem(id); |
| 917 | } |
| @@ -1457,10 +1527,11 @@ | |
| 1527 | n: nFetch, |
| 1528 | i: iFirst |
| 1529 | }, |
| 1530 | responseType: "json", |
| 1531 | onload:function(jx){ |
| 1532 | reportConnectionReestablished(); |
| 1533 | if( bDown ) jx.msgs.reverse(); |
| 1534 | jx.msgs.forEach((m) => { |
| 1535 | m.isSearchResult = true; |
| 1536 | var mw = new Chat.MessageWidget(m); |
| 1537 | if( bDown ){ |
| @@ -1624,10 +1695,51 @@ | |
| 1695 | const theMsg = findMessageWidgetParent(w); |
| 1696 | if(theMsg) Chat.deleteMessageElem(theMsg); |
| 1697 | })); |
| 1698 | Chat.reportErrorAsMessage(w); |
| 1699 | }; |
| 1700 | |
| 1701 | const removeConnectionErrors = function() { |
| 1702 | D.remove(Chat.e.viewMessages.querySelectorAll( |
| 1703 | '.message-widget.error-connection')); |
| 1704 | }; |
| 1705 | |
| 1706 | /* Assume the connection has been established, reset |
| 1707 | the Chat.timer.tidReconnect, and alert the user |
| 1708 | that the outage appears to be over. */ |
| 1709 | const reportConnectionReestablished = function(){ |
| 1710 | if( Chat.timer.tidReconnect ){ |
| 1711 | clearTimeout(Chat.timer.tidReconnect); |
| 1712 | Chat.timer.tidReconnect = 0; |
| 1713 | } |
| 1714 | if( Chat.timer.isDelayed() ){ |
| 1715 | removeConnectionErrors(); |
| 1716 | Chat.timer.resetDelay(); |
| 1717 | Chat.reportReconnection( |
| 1718 | "Connection restored after outage." |
| 1719 | ); |
| 1720 | setTimeout( Chat.poll, 0 ); |
| 1721 | } |
| 1722 | }; |
| 1723 | |
| 1724 | /* To be called from F.fetch() beforesend() handlers. If we're |
| 1725 | currently in delayed-retry mode and a connection is start, try to |
| 1726 | reset the delay after N time waiting on that connection. The fact |
| 1727 | that the connection is waiting to respond, rather than outright |
| 1728 | failing, is a good hint that the outage is over and we can reset |
| 1729 | the back-off timer. */ |
| 1730 | const setupConnectionReestablished = function(){ |
| 1731 | if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){ |
| 1732 | Chat.timer.tidReconnect = setTimeout(()=>{ |
| 1733 | Chat.timer.tidReconnect = 0; |
| 1734 | if( poll.running ){ |
| 1735 | reportConnectionReestablished(); |
| 1736 | } |
| 1737 | }, Chat.timer.$initialDelay * 5 ); |
| 1738 | removeConnectionErrors(); |
| 1739 | } |
| 1740 | }; |
| 1741 | |
| 1742 | /** |
| 1743 | Submits the contents of the message input field (if not empty) |
| 1744 | and/or the file attachment field to the server. If both are |
| 1745 | empty, this is a no-op. |
| @@ -1686,10 +1798,11 @@ | |
| 1798 | onerror:function(err){ |
| 1799 | self.reportErrorAsMessage(err); |
| 1800 | recoverFailedMessage(fallback); |
| 1801 | }, |
| 1802 | onload:function(txt){ |
| 1803 | reportConnectionReestablished(); |
| 1804 | if(!txt) return/*success response*/; |
| 1805 | try{ |
| 1806 | const json = JSON.parse(txt); |
| 1807 | self.newContent({msgs:[json]}); |
| 1808 | }catch(e){ |
| @@ -2185,10 +2298,11 @@ | |
| 2298 | /*filename needed for mimetype determination*/); |
| 2299 | fd.append('render_mode',F.page.previewModes.wiki); |
| 2300 | F.fetch('ajax/preview-text',{ |
| 2301 | payload: fd, |
| 2302 | onload: function(html){ |
| 2303 | reportConnectionReestablished(); |
| 2304 | Chat.setPreviewText(html); |
| 2305 | F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr')); |
| 2306 | }, |
| 2307 | onerror: function(e){ |
| 2308 | F.fetch.onerror(e); |
| @@ -2322,10 +2436,11 @@ | |
| 2436 | onerror:function(err){ |
| 2437 | Chat.reportErrorAsMessage(err); |
| 2438 | Chat._isBatchLoading = false; |
| 2439 | }, |
| 2440 | onload:function(x){ |
| 2441 | reportConnectionReestablished(); |
| 2442 | let gotMessages = x.msgs.length; |
| 2443 | newcontent(x,true); |
| 2444 | Chat._isBatchLoading = false; |
| 2445 | Chat.updateActiveUserList(); |
| 2446 | if(Chat._gotServerError){ |
| @@ -2411,10 +2526,11 @@ | |
| 2526 | onerror:function(err){ |
| 2527 | Chat.setCurrentView(Chat.e.viewMessages); |
| 2528 | Chat.reportErrorAsMessage(err); |
| 2529 | }, |
| 2530 | onload:function(jx){ |
| 2531 | reportConnectionReestablished(); |
| 2532 | let previd = 0; |
| 2533 | D.clearElement(eMsgTgt); |
| 2534 | jx.msgs.forEach((m)=>{ |
| 2535 | m.isSearchResult = true; |
| 2536 | const mw = new Chat.MessageWidget(m); |
| @@ -2444,29 +2560,62 @@ | |
| 2560 | } |
| 2561 | } |
| 2562 | ); |
| 2563 | }/*Chat.submitSearch()*/; |
| 2564 | |
| 2565 | /** |
| 2566 | Deal with the last poll() response and maybe re-start poll(). |
| 2567 | */ |
| 2568 | const afterPollFetch = function f(err){ |
| 2569 | if(true===f.isFirstCall){ |
| 2570 | f.isFirstCall = false; |
| 2571 | Chat.ajaxEnd(); |
| 2572 | Chat.e.viewMessages.classList.remove('loading'); |
| 2573 | setTimeout(function(){ |
| 2574 | Chat.scrollMessagesTo(1); |
| 2575 | }, 250); |
| 2576 | } |
| 2577 | if(Chat.timer.tidPoller) { |
| 2578 | clearTimeout(Chat.timer.tidPoller); |
| 2579 | Chat.timer.tidPoller = 0; |
| 2580 | } |
| 2581 | if(Chat._gotServerError){ |
| 2582 | Chat.reportErrorAsMessage( |
| 2583 | "Shutting down chat poller due to server-side error. ", |
| 2584 | "Reload this page to reactivate it." |
| 2585 | ); |
| 2586 | Chat.timer.tidPoller = undefined; |
| 2587 | poll.running = false; |
| 2588 | } else { |
| 2589 | poll.running = false; |
| 2590 | if( !err ){ |
| 2591 | /* Restart the poller. */ |
| 2592 | Chat.timer.tidPoller = setTimeout( |
| 2593 | poll, Chat.timer.resetDelay() |
| 2594 | ); |
| 2595 | }else{ |
| 2596 | /* Delay a while before trying again, noting that other Chat |
| 2597 | APIs may try and succeed at connections before this timer |
| 2598 | resolves. */ |
| 2599 | const delay = Chat.timer.incrDelay(); |
| 2600 | const msg = D.addClass( |
| 2601 | Chat.reportErrorAsMessage( |
| 2602 | "Connection error. Retrying in ", |
| 2603 | delay, " ms." |
| 2604 | ).e.body, |
| 2605 | 'error-connection' |
| 2606 | ); |
| 2607 | Chat.timer.tidPoller = setTimeout(()=>{ |
| 2608 | D.remove(msg); |
| 2609 | poll(); |
| 2610 | }, delay); |
| 2611 | } |
| 2612 | //console.log("isOkay =",isOkay,"currentDelay =",Chat.timer.currentDelay); |
| 2613 | } |
| 2614 | }; |
| 2615 | afterPollFetch.isFirstCall = true; |
| 2616 | |
| 2617 | /** |
| 2618 | FIXME: when polling fails because the remote server is |
| 2619 | reachable but it's not accepting HTTP requests, we should back |
| 2620 | off on polling for a while. e.g. if the remote web server process |
| 2621 | is killed, the poll fails quickly and immediately retries, |
| @@ -2489,42 +2638,48 @@ | |
| 2638 | Chat.ajaxStart(); |
| 2639 | Chat.e.viewMessages.classList.add('loading'); |
| 2640 | } |
| 2641 | F.fetch("chat-poll",{ |
| 2642 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 2643 | //timeout: 8000, |
| 2644 | urlParams:{ |
| 2645 | name: Chat.mxMsg |
| 2646 | }, |
| 2647 | responseType: "json", |
| 2648 | // Disable the ajax start/end handling for this long-polling op: |
| 2649 | beforesend: function(){ |
| 2650 | setupConnectionReestablished(); |
| 2651 | }, |
| 2652 | aftersend: function(){ |
| 2653 | }, |
| 2654 | onerror:function(err){ |
| 2655 | Chat._isBatchLoading = false; |
| 2656 | if(Chat.verboseErrors) console.error("poll onerror:",err); |
| 2657 | /* ^^^ we don't use Chat.reportError() here b/c the polling |
| 2658 | fails exepectedly when it times out, but is then immediately |
| 2659 | resumed, and reportError() produces a loud error message. */ |
| 2660 | afterPollFetch(err); |
| 2661 | }, |
| 2662 | onload:function(y){ |
| 2663 | reportConnectionReestablished(); |
| 2664 | newcontent(y); |
| 2665 | if(Chat._isBatchLoading){ |
| 2666 | Chat._isBatchLoading = false; |
| 2667 | Chat.updateActiveUserList(); |
| 2668 | } |
| 2669 | afterPollFetch(); |
| 2670 | } |
| 2671 | }); |
| 2672 | }; |
| 2673 | poll.isFirstCall = true; |
| 2674 | Chat.poll = poll; |
| 2675 | Chat._gotServerError = poll.running = false; |
| 2676 | if( window.fossil.config.chat.fromcli ){ |
| 2677 | Chat.chatOnlyMode(true); |
| 2678 | } |
| 2679 | Chat.timer.tidPoller = setTimeout(poll, Chat.timer.resetDelay()); |
| 2680 | delete ForceResizeKludge.$disabled; |
| 2681 | ForceResizeKludge(); |
| 2682 | Chat.animate.$disabled = false; |
| 2683 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2684 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2685 | }); |
| 2686 |