Fossil SCM
Maintainability cleanups and docs in /chat. No (intended) functional changes.
Commit
084001c76dba674baa6bc4963e561ec4b500fb07f1122303704bfefd8f7b304c
Parent
1d3db5050fead70…
1 file changed
+87
-52
+87
-52
| --- src/fossil.page.chat.js | ||
| +++ src/fossil.page.chat.js | ||
| @@ -196,19 +196,23 @@ | ||
| 196 | 196 | The timing of resetting the delay when service returns is, |
| 197 | 197 | because of the long-poll connection and our lack of low-level |
| 198 | 198 | insight into the connection at this level, a bit wonky. |
| 199 | 199 | */ |
| 200 | 200 | timer:{ |
| 201 | - tidPoller: undefined /* setTimeout() poller timer id */, | |
| 201 | + /* setTimeout() ID for (delayed) starting a Chat.poll(), so | |
| 202 | + that it runs at controlled intervals (which change when a | |
| 203 | + connection drops and recovers). */ | |
| 204 | + tidPendingPoll: undefined, | |
| 205 | + tidClearPollErr: undefined /*setTimeout() timer id for | |
| 206 | + reconnection determination. See | |
| 207 | + clearPollErrOnWait(). */, | |
| 202 | 208 | $initialDelay: 1000 /* initial polling interval (ms) */, |
| 203 | 209 | currentDelay: 1000 /* current polling interval */, |
| 204 | 210 | maxDelay: 60000 * 5 /* max interval when backing off for |
| 205 | 211 | connection errors */, |
| 206 | - minDelay: 5000 /* minimum delay time */, | |
| 207 | - tidReconnect: undefined /*setTimeout() timer id for | |
| 208 | - reconnection determination. See | |
| 209 | - clearPollErrOnWait(). */, | |
| 212 | + minDelay: 5000 /* minimum delay time for a back-off/retry | |
| 213 | + attempt. */, | |
| 210 | 214 | errCount: 0 /* Current poller connection error count */, |
| 211 | 215 | minErrForNotify: 4 /* Don't warn for connection errors until this |
| 212 | 216 | many have occurred */, |
| 213 | 217 | pollTimeout: (1 && window.location.hostname.match( |
| 214 | 218 | "localhost" /*presumably local dev mode*/ |
| @@ -243,10 +247,40 @@ | ||
| 243 | 247 | return this.currentDelay = ms || this.$initialDelay; |
| 244 | 248 | }, |
| 245 | 249 | /** Returns true if the timer is set to delayed mode. */ |
| 246 | 250 | isDelayed: function(){ |
| 247 | 251 | return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0; |
| 252 | + }, | |
| 253 | + /** | |
| 254 | + Cancels any in-progress pending-poll timer and starts a new | |
| 255 | + one with the given delay, defaulting to this.resetDelay(). | |
| 256 | + */ | |
| 257 | + startPendingPollTimer: function(delay){ | |
| 258 | + this.cancelPendingPollTimer().tidPendingPoll | |
| 259 | + = setTimeout( Chat.poll, delay || Chat.timer.resetDelay() ); | |
| 260 | + return this; | |
| 261 | + }, | |
| 262 | + /** | |
| 263 | + Cancels any still-active timer set to trigger the next | |
| 264 | + Chat.poll(). | |
| 265 | + */ | |
| 266 | + cancelPendingPollTimer: function(){ | |
| 267 | + if( this.tidPendingPoll ){ | |
| 268 | + clearTimeout(this.tidPendingPoll); | |
| 269 | + this.tidPendingPoll = 0; | |
| 270 | + } | |
| 271 | + return this; | |
| 272 | + }, | |
| 273 | + /** | |
| 274 | + Cancels any pending reconnection attempt back-off timer.. | |
| 275 | + */ | |
| 276 | + cancelReconnectCheckTimer: function(){ | |
| 277 | + if( this.tidClearPollErr ){ | |
| 278 | + clearTimeout(this.tidClearPollErr); | |
| 279 | + this.tidClearPollErr = 0; | |
| 280 | + } | |
| 281 | + return this; | |
| 248 | 282 | } |
| 249 | 283 | }, |
| 250 | 284 | /** |
| 251 | 285 | Gets (no args) or sets (1 arg) the current input text field |
| 252 | 286 | value, taking into account single- vs multi-line input. The |
| @@ -1750,31 +1784,27 @@ | ||
| 1750 | 1784 | })); |
| 1751 | 1785 | D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message"); |
| 1752 | 1786 | }; |
| 1753 | 1787 | |
| 1754 | 1788 | /* Assume the connection has been established, reset the |
| 1755 | - Chat.timer.tidReconnect, and (if showMsg and | |
| 1789 | + Chat.timer.tidClearPollErr, and (if showMsg and | |
| 1756 | 1790 | !!Chat.e.eMsgPollError) alert the user that the outage appears to |
| 1757 | 1791 | be over. Also schedule Chat.poll() to run in the very near |
| 1758 | 1792 | future. */ |
| 1759 | 1793 | const reportConnectionOkay = function(dbgContext, showMsg = true){ |
| 1760 | 1794 | if(Chat.beVerbose){ |
| 1761 | 1795 | console.warn('reportConnectionOkay', dbgContext, |
| 1762 | 1796 | 'Chat.e.pollErrorMarker classes =', |
| 1763 | 1797 | Chat.e.pollErrorMarker.classList, |
| 1764 | - 'Chat.timer.tidReconnect =',Chat.timer.tidReconnect, | |
| 1798 | + 'Chat.timer.tidClearPollErr =',Chat.timer.tidClearPollErr, | |
| 1765 | 1799 | 'Chat.timer =',Chat.timer); |
| 1766 | 1800 | } |
| 1767 | - setTimeout( Chat.poll, Chat.timer.resetDelay() ); | |
| 1768 | 1801 | if( Chat.timer.errCount ){ |
| 1769 | 1802 | D.removeClass(Chat.e.pollErrorMarker, 'connection-error'); |
| 1770 | 1803 | Chat.timer.errCount = 0; |
| 1771 | 1804 | } |
| 1772 | - if( Chat.timer.tidReconnect ){ | |
| 1773 | - clearTimeout(Chat.timer.tidReconnect); | |
| 1774 | - Chat.timer.tidReconnect = 0; | |
| 1775 | - } | |
| 1805 | + Chat.timer.cancelReconnectCheckTimer().startPendingPollTimer(); | |
| 1776 | 1806 | if( Chat.e.eMsgPollError ) { |
| 1777 | 1807 | const oldErrMsg = Chat.e.eMsgPollError; |
| 1778 | 1808 | Chat.e.eMsgPollError = undefined; |
| 1779 | 1809 | if( showMsg ){ |
| 1780 | 1810 | if(Chat.beVerbose){ |
| @@ -2611,36 +2641,44 @@ | ||
| 2611 | 2641 | } |
| 2612 | 2642 | } |
| 2613 | 2643 | ); |
| 2614 | 2644 | }/*Chat.submitSearch()*/; |
| 2615 | 2645 | |
| 2616 | - /* To be called from F.fetch('chat-poll') beforesend() handler. If | |
| 2617 | - we're currently in delayed-retry mode and a connection is | |
| 2618 | - started, try to reset the delay after N time waiting on that | |
| 2619 | - connection. The fact that the connection is waiting to respond, | |
| 2620 | - rather than outright failing, is a good hint that the outage is | |
| 2621 | - over and we can reset the back-off timer. | |
| 2622 | - | |
| 2623 | - Without this, recovery of a connection error won't be reported | |
| 2624 | - until after the long-poll completes by either receiving new | |
| 2625 | - messages or times out. Once a long-poll is in progress, though, | |
| 2626 | - we "know" that it's up and running again, so can update the UI | |
| 2627 | - and connection timer to reflect that. | |
| 2646 | + /* | |
| 2647 | + To be called from F.fetch('chat-poll') beforesend() handler. If | |
| 2648 | + we're currently in delayed-retry mode and a connection is | |
| 2649 | + started, try to reset the delay after N time waiting on that | |
| 2650 | + connection. The fact that the connection is waiting to respond, | |
| 2651 | + rather than outright failing, is a good hint that the outage is | |
| 2652 | + over and we can reset the back-off timer. | |
| 2653 | + | |
| 2654 | + Without this, recovery of a connection error won't be reported | |
| 2655 | + until after the long-poll completes by either receiving new | |
| 2656 | + messages or timing out. Once a long-poll is in progress, though, | |
| 2657 | + we "know" that it's up and running again, so can update the UI and | |
| 2658 | + connection timer to reflect that. That's the job this function | |
| 2659 | + does. | |
| 2660 | + | |
| 2661 | + Only one of these asynchronous checks will ever be active | |
| 2662 | + concurrently and only if Chat.timer.isDelayed() is true. i.e. if | |
| 2663 | + this timer is active or Chat.timer.isDelayed() is false, this is a | |
| 2664 | + no-op. | |
| 2628 | 2665 | */ |
| 2629 | 2666 | const chatPollBeforeSend = function(){ |
| 2630 | - //console.warn('chatPollBeforeSend outer', Chat.timer.tidReconnect, Chat.timer.currentDelay); | |
| 2631 | - if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){ | |
| 2632 | - Chat.timer.tidReconnect = setTimeout(()=>{ | |
| 2667 | + //console.warn('chatPollBeforeSend outer', Chat.timer.tidClearPollErr, Chat.timer.currentDelay); | |
| 2668 | + if( !Chat.timer.tidClearPollErr && Chat.timer.isDelayed() ){ | |
| 2669 | + Chat.timer.tidClearPollErr = setTimeout(()=>{ | |
| 2633 | 2670 | //console.warn('chatPollBeforeSend inner'); |
| 2634 | - Chat.timer.tidReconnect = 0; | |
| 2671 | + Chat.timer.tidClearPollErr = 0; | |
| 2635 | 2672 | if( poll.running ){ |
| 2636 | 2673 | /* This chat-poll F.fetch() is still underway, so let's |
| 2637 | 2674 | assume the connection is back up until/unless it times |
| 2638 | 2675 | out or breaks again. */ |
| 2639 | 2676 | reportConnectionOkay('chatPollBeforeSend', true); |
| 2640 | 2677 | } |
| 2641 | - }, Chat.timer.$initialDelay * 3 ); | |
| 2678 | + }, Chat.timer.$initialDelay * 4/*kinda arbitrary: not too long for UI wait and | |
| 2679 | + not too short as to make connection unlikely. */ ); | |
| 2642 | 2680 | } |
| 2643 | 2681 | }; |
| 2644 | 2682 | |
| 2645 | 2683 | /** |
| 2646 | 2684 | Deal with the last poll() response and maybe re-start poll(). |
| @@ -2652,20 +2690,16 @@ | ||
| 2652 | 2690 | Chat.e.viewMessages.classList.remove('loading'); |
| 2653 | 2691 | setTimeout(function(){ |
| 2654 | 2692 | Chat.scrollMessagesTo(1); |
| 2655 | 2693 | }, 250); |
| 2656 | 2694 | } |
| 2657 | - if(Chat.timer.tidPoller) { | |
| 2658 | - clearTimeout(Chat.timer.tidPoller); | |
| 2659 | - Chat.timer.tidPoller = 0; | |
| 2660 | - } | |
| 2695 | + Chat.timer.cancelPendingPollTimer(); | |
| 2661 | 2696 | if(Chat._gotServerError){ |
| 2662 | 2697 | Chat.reportErrorAsMessage( |
| 2663 | 2698 | "Shutting down chat poller due to server-side error. ", |
| 2664 | 2699 | "Reload this page to reactivate it." |
| 2665 | 2700 | ); |
| 2666 | - Chat.timer.tidPoller = undefined; | |
| 2667 | 2701 | } else { |
| 2668 | 2702 | if( err && Chat.beVerbose ){ |
| 2669 | 2703 | console.error("afterPollFetch:",err.name,err.status,err.message); |
| 2670 | 2704 | } |
| 2671 | 2705 | if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){ |
| @@ -2701,42 +2735,44 @@ | ||
| 2701 | 2735 | } |
| 2702 | 2736 | const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); |
| 2703 | 2737 | D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); |
| 2704 | 2738 | /* Add a "retry now" button */ |
| 2705 | 2739 | const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); |
| 2706 | - D.append(Chat.e.eMsgPollError.e.content, " ", btnDel); | |
| 2740 | + const eParent = Chat.e.eMsgPollError.e.content; | |
| 2741 | + D.append(eParent, " ", btnDel); | |
| 2707 | 2742 | btnDel.addEventListener('click', function(){ |
| 2708 | 2743 | D.remove(btnDel); |
| 2709 | - Chat.timer.currentDelay = | |
| 2710 | - Chat.timer.resetDelay() + 1 /*workaround for showing the "connection restored" message*/; | |
| 2711 | - if( Chat.timer.tidPoller ){ | |
| 2712 | - clearTimeout(Chat.timer.tidPoller); | |
| 2713 | - Chat.timer.tidPoller = 0; | |
| 2714 | - } | |
| 2744 | + D.append(eParent, D.text("retrying...")); | |
| 2745 | + Chat.timer.cancelPendingPollTimer().currentDelay = | |
| 2746 | + Chat.timer.resetDelay() + | |
| 2747 | + 1 /*workaround for showing the "connection restored" | |
| 2748 | + message, as the +1 will cause | |
| 2749 | + Chat.timer.isDelayed() to be true.*/; | |
| 2715 | 2750 | poll(); |
| 2716 | 2751 | }); |
| 2717 | 2752 | //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction |
| 2718 | 2753 | } |
| 2719 | - Chat.timer.tidPoller = setTimeout(Chat.poll, delay); | |
| 2754 | + Chat.timer.startPendingPollTimer(delay); | |
| 2720 | 2755 | } |
| 2721 | 2756 | } |
| 2722 | 2757 | }; |
| 2723 | 2758 | afterPollFetch.isFirstCall = true; |
| 2724 | 2759 | |
| 2725 | 2760 | /** |
| 2726 | 2761 | Initiates, if it's not already running, a single long-poll |
| 2727 | 2762 | request to the /chat-poll endpoint. In the handling of that |
| 2728 | 2763 | response, it end up will psuedo-recursively calling itself via |
| 2729 | - the response-handling process. | |
| 2764 | + the response-handling process. Despite being async, the implied | |
| 2765 | + returned Promise is meaningless. | |
| 2730 | 2766 | */ |
| 2731 | 2767 | const poll = Chat.poll = async function f(){ |
| 2732 | 2768 | if(f.running) return; |
| 2733 | 2769 | f.running = true; |
| 2734 | 2770 | Chat._isBatchLoading = f.isFirstCall; |
| 2735 | 2771 | if(true===f.isFirstCall){ |
| 2736 | 2772 | f.isFirstCall = false; |
| 2737 | - Chat.pendingOnError = undefined; | |
| 2773 | + f.pendingOnError = undefined; | |
| 2738 | 2774 | Chat.ajaxStart(); |
| 2739 | 2775 | Chat.e.viewMessages.classList.add('loading'); |
| 2740 | 2776 | /* |
| 2741 | 2777 | We manager onerror() results in poll() in a roundabout |
| 2742 | 2778 | manner: when an onerror() arrives, we stash it aside |
| @@ -2757,18 +2793,17 @@ | ||
| 2757 | 2793 | this one is a lot less explicable. (It's almost certainly a |
| 2758 | 2794 | mis-handling bug in F.fetch(), but it has so far eluded my |
| 2759 | 2795 | eyes.) |
| 2760 | 2796 | */ |
| 2761 | 2797 | f.delayPendingOnError = function(err){ |
| 2762 | - if( Chat.pendingOnError ){ | |
| 2763 | - const x = Chat.pendingOnError; | |
| 2764 | - Chat.pendingOnError = undefined; | |
| 2798 | + if( f.pendingOnError ){ | |
| 2799 | + const x = f.pendingOnError; | |
| 2800 | + f.pendingOnError = undefined; | |
| 2765 | 2801 | afterPollFetch(x); |
| 2766 | 2802 | } |
| 2767 | 2803 | }; |
| 2768 | 2804 | } |
| 2769 | - let nErr = 0; | |
| 2770 | 2805 | F.fetch("chat-poll",{ |
| 2771 | 2806 | timeout: Chat.timer.pollTimeout, |
| 2772 | 2807 | urlParams:{ |
| 2773 | 2808 | name: Chat.mxMsg |
| 2774 | 2809 | }, |
| @@ -2777,20 +2812,20 @@ | ||
| 2777 | 2812 | beforesend: chatPollBeforeSend, |
| 2778 | 2813 | aftersend: function(){ |
| 2779 | 2814 | poll.running = false; |
| 2780 | 2815 | }, |
| 2781 | 2816 | ontimeout: function(err){ |
| 2782 | - Chat.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/; | |
| 2817 | + f.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/; | |
| 2783 | 2818 | afterPollFetch(err); |
| 2784 | 2819 | }, |
| 2785 | 2820 | onerror:function(err){ |
| 2786 | 2821 | Chat._isBatchLoading = false; |
| 2787 | 2822 | if(Chat.beVerbose){ |
| 2788 | 2823 | console.error("poll.onerror:",err.name,err.status,JSON.stringify(err)); |
| 2789 | 2824 | } |
| 2790 | - Chat.pendingOnError = err; | |
| 2791 | - setTimeout(f.delayPendingOnError, 250); | |
| 2825 | + f.pendingOnError = err; | |
| 2826 | + setTimeout(f.delayPendingOnError, 100); | |
| 2792 | 2827 | }, |
| 2793 | 2828 | onload:function(y){ |
| 2794 | 2829 | reportConnectionOkay('poll.onload', true); |
| 2795 | 2830 | newcontent(y); |
| 2796 | 2831 | if(Chat._isBatchLoading){ |
| @@ -2804,12 +2839,12 @@ | ||
| 2804 | 2839 | poll.isFirstCall = true; |
| 2805 | 2840 | Chat._gotServerError = poll.running = false; |
| 2806 | 2841 | if( window.fossil.config.chat.fromcli ){ |
| 2807 | 2842 | Chat.chatOnlyMode(true); |
| 2808 | 2843 | } |
| 2809 | - Chat.timer.tidPoller = setTimeout(poll, Chat.timer.resetDelay()); | |
| 2844 | + Chat.timer.startPendingPollTimer(); | |
| 2810 | 2845 | delete ForceResizeKludge.$disabled; |
| 2811 | 2846 | ForceResizeKludge(); |
| 2812 | 2847 | Chat.animate.$disabled = false; |
| 2813 | 2848 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2814 | 2849 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2815 | 2850 | }); |
| 2816 | 2851 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -196,19 +196,23 @@ | |
| 196 | The timing of resetting the delay when service returns is, |
| 197 | because of the long-poll connection and our lack of low-level |
| 198 | insight into the connection at this level, a bit wonky. |
| 199 | */ |
| 200 | timer:{ |
| 201 | tidPoller: undefined /* setTimeout() poller timer id */, |
| 202 | $initialDelay: 1000 /* initial polling interval (ms) */, |
| 203 | currentDelay: 1000 /* current polling interval */, |
| 204 | maxDelay: 60000 * 5 /* max interval when backing off for |
| 205 | connection errors */, |
| 206 | minDelay: 5000 /* minimum delay time */, |
| 207 | tidReconnect: undefined /*setTimeout() timer id for |
| 208 | reconnection determination. See |
| 209 | clearPollErrOnWait(). */, |
| 210 | errCount: 0 /* Current poller connection error count */, |
| 211 | minErrForNotify: 4 /* Don't warn for connection errors until this |
| 212 | many have occurred */, |
| 213 | pollTimeout: (1 && window.location.hostname.match( |
| 214 | "localhost" /*presumably local dev mode*/ |
| @@ -243,10 +247,40 @@ | |
| 243 | return this.currentDelay = ms || this.$initialDelay; |
| 244 | }, |
| 245 | /** Returns true if the timer is set to delayed mode. */ |
| 246 | isDelayed: function(){ |
| 247 | return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0; |
| 248 | } |
| 249 | }, |
| 250 | /** |
| 251 | Gets (no args) or sets (1 arg) the current input text field |
| 252 | value, taking into account single- vs multi-line input. The |
| @@ -1750,31 +1784,27 @@ | |
| 1750 | })); |
| 1751 | D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message"); |
| 1752 | }; |
| 1753 | |
| 1754 | /* Assume the connection has been established, reset the |
| 1755 | Chat.timer.tidReconnect, and (if showMsg and |
| 1756 | !!Chat.e.eMsgPollError) alert the user that the outage appears to |
| 1757 | be over. Also schedule Chat.poll() to run in the very near |
| 1758 | future. */ |
| 1759 | const reportConnectionOkay = function(dbgContext, showMsg = true){ |
| 1760 | if(Chat.beVerbose){ |
| 1761 | console.warn('reportConnectionOkay', dbgContext, |
| 1762 | 'Chat.e.pollErrorMarker classes =', |
| 1763 | Chat.e.pollErrorMarker.classList, |
| 1764 | 'Chat.timer.tidReconnect =',Chat.timer.tidReconnect, |
| 1765 | 'Chat.timer =',Chat.timer); |
| 1766 | } |
| 1767 | setTimeout( Chat.poll, Chat.timer.resetDelay() ); |
| 1768 | if( Chat.timer.errCount ){ |
| 1769 | D.removeClass(Chat.e.pollErrorMarker, 'connection-error'); |
| 1770 | Chat.timer.errCount = 0; |
| 1771 | } |
| 1772 | if( Chat.timer.tidReconnect ){ |
| 1773 | clearTimeout(Chat.timer.tidReconnect); |
| 1774 | Chat.timer.tidReconnect = 0; |
| 1775 | } |
| 1776 | if( Chat.e.eMsgPollError ) { |
| 1777 | const oldErrMsg = Chat.e.eMsgPollError; |
| 1778 | Chat.e.eMsgPollError = undefined; |
| 1779 | if( showMsg ){ |
| 1780 | if(Chat.beVerbose){ |
| @@ -2611,36 +2641,44 @@ | |
| 2611 | } |
| 2612 | } |
| 2613 | ); |
| 2614 | }/*Chat.submitSearch()*/; |
| 2615 | |
| 2616 | /* To be called from F.fetch('chat-poll') beforesend() handler. If |
| 2617 | we're currently in delayed-retry mode and a connection is |
| 2618 | started, try to reset the delay after N time waiting on that |
| 2619 | connection. The fact that the connection is waiting to respond, |
| 2620 | rather than outright failing, is a good hint that the outage is |
| 2621 | over and we can reset the back-off timer. |
| 2622 | |
| 2623 | Without this, recovery of a connection error won't be reported |
| 2624 | until after the long-poll completes by either receiving new |
| 2625 | messages or times out. Once a long-poll is in progress, though, |
| 2626 | we "know" that it's up and running again, so can update the UI |
| 2627 | and connection timer to reflect that. |
| 2628 | */ |
| 2629 | const chatPollBeforeSend = function(){ |
| 2630 | //console.warn('chatPollBeforeSend outer', Chat.timer.tidReconnect, Chat.timer.currentDelay); |
| 2631 | if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){ |
| 2632 | Chat.timer.tidReconnect = setTimeout(()=>{ |
| 2633 | //console.warn('chatPollBeforeSend inner'); |
| 2634 | Chat.timer.tidReconnect = 0; |
| 2635 | if( poll.running ){ |
| 2636 | /* This chat-poll F.fetch() is still underway, so let's |
| 2637 | assume the connection is back up until/unless it times |
| 2638 | out or breaks again. */ |
| 2639 | reportConnectionOkay('chatPollBeforeSend', true); |
| 2640 | } |
| 2641 | }, Chat.timer.$initialDelay * 3 ); |
| 2642 | } |
| 2643 | }; |
| 2644 | |
| 2645 | /** |
| 2646 | Deal with the last poll() response and maybe re-start poll(). |
| @@ -2652,20 +2690,16 @@ | |
| 2652 | Chat.e.viewMessages.classList.remove('loading'); |
| 2653 | setTimeout(function(){ |
| 2654 | Chat.scrollMessagesTo(1); |
| 2655 | }, 250); |
| 2656 | } |
| 2657 | if(Chat.timer.tidPoller) { |
| 2658 | clearTimeout(Chat.timer.tidPoller); |
| 2659 | Chat.timer.tidPoller = 0; |
| 2660 | } |
| 2661 | if(Chat._gotServerError){ |
| 2662 | Chat.reportErrorAsMessage( |
| 2663 | "Shutting down chat poller due to server-side error. ", |
| 2664 | "Reload this page to reactivate it." |
| 2665 | ); |
| 2666 | Chat.timer.tidPoller = undefined; |
| 2667 | } else { |
| 2668 | if( err && Chat.beVerbose ){ |
| 2669 | console.error("afterPollFetch:",err.name,err.status,err.message); |
| 2670 | } |
| 2671 | if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){ |
| @@ -2701,42 +2735,44 @@ | |
| 2701 | } |
| 2702 | const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); |
| 2703 | D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); |
| 2704 | /* Add a "retry now" button */ |
| 2705 | const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); |
| 2706 | D.append(Chat.e.eMsgPollError.e.content, " ", btnDel); |
| 2707 | btnDel.addEventListener('click', function(){ |
| 2708 | D.remove(btnDel); |
| 2709 | Chat.timer.currentDelay = |
| 2710 | Chat.timer.resetDelay() + 1 /*workaround for showing the "connection restored" message*/; |
| 2711 | if( Chat.timer.tidPoller ){ |
| 2712 | clearTimeout(Chat.timer.tidPoller); |
| 2713 | Chat.timer.tidPoller = 0; |
| 2714 | } |
| 2715 | poll(); |
| 2716 | }); |
| 2717 | //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction |
| 2718 | } |
| 2719 | Chat.timer.tidPoller = setTimeout(Chat.poll, delay); |
| 2720 | } |
| 2721 | } |
| 2722 | }; |
| 2723 | afterPollFetch.isFirstCall = true; |
| 2724 | |
| 2725 | /** |
| 2726 | Initiates, if it's not already running, a single long-poll |
| 2727 | request to the /chat-poll endpoint. In the handling of that |
| 2728 | response, it end up will psuedo-recursively calling itself via |
| 2729 | the response-handling process. |
| 2730 | */ |
| 2731 | const poll = Chat.poll = async function f(){ |
| 2732 | if(f.running) return; |
| 2733 | f.running = true; |
| 2734 | Chat._isBatchLoading = f.isFirstCall; |
| 2735 | if(true===f.isFirstCall){ |
| 2736 | f.isFirstCall = false; |
| 2737 | Chat.pendingOnError = undefined; |
| 2738 | Chat.ajaxStart(); |
| 2739 | Chat.e.viewMessages.classList.add('loading'); |
| 2740 | /* |
| 2741 | We manager onerror() results in poll() in a roundabout |
| 2742 | manner: when an onerror() arrives, we stash it aside |
| @@ -2757,18 +2793,17 @@ | |
| 2757 | this one is a lot less explicable. (It's almost certainly a |
| 2758 | mis-handling bug in F.fetch(), but it has so far eluded my |
| 2759 | eyes.) |
| 2760 | */ |
| 2761 | f.delayPendingOnError = function(err){ |
| 2762 | if( Chat.pendingOnError ){ |
| 2763 | const x = Chat.pendingOnError; |
| 2764 | Chat.pendingOnError = undefined; |
| 2765 | afterPollFetch(x); |
| 2766 | } |
| 2767 | }; |
| 2768 | } |
| 2769 | let nErr = 0; |
| 2770 | F.fetch("chat-poll",{ |
| 2771 | timeout: Chat.timer.pollTimeout, |
| 2772 | urlParams:{ |
| 2773 | name: Chat.mxMsg |
| 2774 | }, |
| @@ -2777,20 +2812,20 @@ | |
| 2777 | beforesend: chatPollBeforeSend, |
| 2778 | aftersend: function(){ |
| 2779 | poll.running = false; |
| 2780 | }, |
| 2781 | ontimeout: function(err){ |
| 2782 | Chat.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/; |
| 2783 | afterPollFetch(err); |
| 2784 | }, |
| 2785 | onerror:function(err){ |
| 2786 | Chat._isBatchLoading = false; |
| 2787 | if(Chat.beVerbose){ |
| 2788 | console.error("poll.onerror:",err.name,err.status,JSON.stringify(err)); |
| 2789 | } |
| 2790 | Chat.pendingOnError = err; |
| 2791 | setTimeout(f.delayPendingOnError, 250); |
| 2792 | }, |
| 2793 | onload:function(y){ |
| 2794 | reportConnectionOkay('poll.onload', true); |
| 2795 | newcontent(y); |
| 2796 | if(Chat._isBatchLoading){ |
| @@ -2804,12 +2839,12 @@ | |
| 2804 | poll.isFirstCall = true; |
| 2805 | Chat._gotServerError = poll.running = false; |
| 2806 | if( window.fossil.config.chat.fromcli ){ |
| 2807 | Chat.chatOnlyMode(true); |
| 2808 | } |
| 2809 | Chat.timer.tidPoller = setTimeout(poll, Chat.timer.resetDelay()); |
| 2810 | delete ForceResizeKludge.$disabled; |
| 2811 | ForceResizeKludge(); |
| 2812 | Chat.animate.$disabled = false; |
| 2813 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2814 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2815 | }); |
| 2816 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -196,19 +196,23 @@ | |
| 196 | The timing of resetting the delay when service returns is, |
| 197 | because of the long-poll connection and our lack of low-level |
| 198 | insight into the connection at this level, a bit wonky. |
| 199 | */ |
| 200 | timer:{ |
| 201 | /* setTimeout() ID for (delayed) starting a Chat.poll(), so |
| 202 | that it runs at controlled intervals (which change when a |
| 203 | connection drops and recovers). */ |
| 204 | tidPendingPoll: undefined, |
| 205 | tidClearPollErr: undefined /*setTimeout() timer id for |
| 206 | reconnection determination. See |
| 207 | clearPollErrOnWait(). */, |
| 208 | $initialDelay: 1000 /* initial polling interval (ms) */, |
| 209 | currentDelay: 1000 /* current polling interval */, |
| 210 | maxDelay: 60000 * 5 /* max interval when backing off for |
| 211 | connection errors */, |
| 212 | minDelay: 5000 /* minimum delay time for a back-off/retry |
| 213 | attempt. */, |
| 214 | errCount: 0 /* Current poller connection error count */, |
| 215 | minErrForNotify: 4 /* Don't warn for connection errors until this |
| 216 | many have occurred */, |
| 217 | pollTimeout: (1 && window.location.hostname.match( |
| 218 | "localhost" /*presumably local dev mode*/ |
| @@ -243,10 +247,40 @@ | |
| 247 | return this.currentDelay = ms || this.$initialDelay; |
| 248 | }, |
| 249 | /** Returns true if the timer is set to delayed mode. */ |
| 250 | isDelayed: function(){ |
| 251 | return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0; |
| 252 | }, |
| 253 | /** |
| 254 | Cancels any in-progress pending-poll timer and starts a new |
| 255 | one with the given delay, defaulting to this.resetDelay(). |
| 256 | */ |
| 257 | startPendingPollTimer: function(delay){ |
| 258 | this.cancelPendingPollTimer().tidPendingPoll |
| 259 | = setTimeout( Chat.poll, delay || Chat.timer.resetDelay() ); |
| 260 | return this; |
| 261 | }, |
| 262 | /** |
| 263 | Cancels any still-active timer set to trigger the next |
| 264 | Chat.poll(). |
| 265 | */ |
| 266 | cancelPendingPollTimer: function(){ |
| 267 | if( this.tidPendingPoll ){ |
| 268 | clearTimeout(this.tidPendingPoll); |
| 269 | this.tidPendingPoll = 0; |
| 270 | } |
| 271 | return this; |
| 272 | }, |
| 273 | /** |
| 274 | Cancels any pending reconnection attempt back-off timer.. |
| 275 | */ |
| 276 | cancelReconnectCheckTimer: function(){ |
| 277 | if( this.tidClearPollErr ){ |
| 278 | clearTimeout(this.tidClearPollErr); |
| 279 | this.tidClearPollErr = 0; |
| 280 | } |
| 281 | return this; |
| 282 | } |
| 283 | }, |
| 284 | /** |
| 285 | Gets (no args) or sets (1 arg) the current input text field |
| 286 | value, taking into account single- vs multi-line input. The |
| @@ -1750,31 +1784,27 @@ | |
| 1784 | })); |
| 1785 | D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message"); |
| 1786 | }; |
| 1787 | |
| 1788 | /* Assume the connection has been established, reset the |
| 1789 | Chat.timer.tidClearPollErr, and (if showMsg and |
| 1790 | !!Chat.e.eMsgPollError) alert the user that the outage appears to |
| 1791 | be over. Also schedule Chat.poll() to run in the very near |
| 1792 | future. */ |
| 1793 | const reportConnectionOkay = function(dbgContext, showMsg = true){ |
| 1794 | if(Chat.beVerbose){ |
| 1795 | console.warn('reportConnectionOkay', dbgContext, |
| 1796 | 'Chat.e.pollErrorMarker classes =', |
| 1797 | Chat.e.pollErrorMarker.classList, |
| 1798 | 'Chat.timer.tidClearPollErr =',Chat.timer.tidClearPollErr, |
| 1799 | 'Chat.timer =',Chat.timer); |
| 1800 | } |
| 1801 | if( Chat.timer.errCount ){ |
| 1802 | D.removeClass(Chat.e.pollErrorMarker, 'connection-error'); |
| 1803 | Chat.timer.errCount = 0; |
| 1804 | } |
| 1805 | Chat.timer.cancelReconnectCheckTimer().startPendingPollTimer(); |
| 1806 | if( Chat.e.eMsgPollError ) { |
| 1807 | const oldErrMsg = Chat.e.eMsgPollError; |
| 1808 | Chat.e.eMsgPollError = undefined; |
| 1809 | if( showMsg ){ |
| 1810 | if(Chat.beVerbose){ |
| @@ -2611,36 +2641,44 @@ | |
| 2641 | } |
| 2642 | } |
| 2643 | ); |
| 2644 | }/*Chat.submitSearch()*/; |
| 2645 | |
| 2646 | /* |
| 2647 | To be called from F.fetch('chat-poll') beforesend() handler. If |
| 2648 | we're currently in delayed-retry mode and a connection is |
| 2649 | started, try to reset the delay after N time waiting on that |
| 2650 | connection. The fact that the connection is waiting to respond, |
| 2651 | rather than outright failing, is a good hint that the outage is |
| 2652 | over and we can reset the back-off timer. |
| 2653 | |
| 2654 | Without this, recovery of a connection error won't be reported |
| 2655 | until after the long-poll completes by either receiving new |
| 2656 | messages or timing out. Once a long-poll is in progress, though, |
| 2657 | we "know" that it's up and running again, so can update the UI and |
| 2658 | connection timer to reflect that. That's the job this function |
| 2659 | does. |
| 2660 | |
| 2661 | Only one of these asynchronous checks will ever be active |
| 2662 | concurrently and only if Chat.timer.isDelayed() is true. i.e. if |
| 2663 | this timer is active or Chat.timer.isDelayed() is false, this is a |
| 2664 | no-op. |
| 2665 | */ |
| 2666 | const chatPollBeforeSend = function(){ |
| 2667 | //console.warn('chatPollBeforeSend outer', Chat.timer.tidClearPollErr, Chat.timer.currentDelay); |
| 2668 | if( !Chat.timer.tidClearPollErr && Chat.timer.isDelayed() ){ |
| 2669 | Chat.timer.tidClearPollErr = setTimeout(()=>{ |
| 2670 | //console.warn('chatPollBeforeSend inner'); |
| 2671 | Chat.timer.tidClearPollErr = 0; |
| 2672 | if( poll.running ){ |
| 2673 | /* This chat-poll F.fetch() is still underway, so let's |
| 2674 | assume the connection is back up until/unless it times |
| 2675 | out or breaks again. */ |
| 2676 | reportConnectionOkay('chatPollBeforeSend', true); |
| 2677 | } |
| 2678 | }, Chat.timer.$initialDelay * 4/*kinda arbitrary: not too long for UI wait and |
| 2679 | not too short as to make connection unlikely. */ ); |
| 2680 | } |
| 2681 | }; |
| 2682 | |
| 2683 | /** |
| 2684 | Deal with the last poll() response and maybe re-start poll(). |
| @@ -2652,20 +2690,16 @@ | |
| 2690 | Chat.e.viewMessages.classList.remove('loading'); |
| 2691 | setTimeout(function(){ |
| 2692 | Chat.scrollMessagesTo(1); |
| 2693 | }, 250); |
| 2694 | } |
| 2695 | Chat.timer.cancelPendingPollTimer(); |
| 2696 | if(Chat._gotServerError){ |
| 2697 | Chat.reportErrorAsMessage( |
| 2698 | "Shutting down chat poller due to server-side error. ", |
| 2699 | "Reload this page to reactivate it." |
| 2700 | ); |
| 2701 | } else { |
| 2702 | if( err && Chat.beVerbose ){ |
| 2703 | console.error("afterPollFetch:",err.name,err.status,err.message); |
| 2704 | } |
| 2705 | if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){ |
| @@ -2701,42 +2735,44 @@ | |
| 2735 | } |
| 2736 | const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); |
| 2737 | D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); |
| 2738 | /* Add a "retry now" button */ |
| 2739 | const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); |
| 2740 | const eParent = Chat.e.eMsgPollError.e.content; |
| 2741 | D.append(eParent, " ", btnDel); |
| 2742 | btnDel.addEventListener('click', function(){ |
| 2743 | D.remove(btnDel); |
| 2744 | D.append(eParent, D.text("retrying...")); |
| 2745 | Chat.timer.cancelPendingPollTimer().currentDelay = |
| 2746 | Chat.timer.resetDelay() + |
| 2747 | 1 /*workaround for showing the "connection restored" |
| 2748 | message, as the +1 will cause |
| 2749 | Chat.timer.isDelayed() to be true.*/; |
| 2750 | poll(); |
| 2751 | }); |
| 2752 | //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction |
| 2753 | } |
| 2754 | Chat.timer.startPendingPollTimer(delay); |
| 2755 | } |
| 2756 | } |
| 2757 | }; |
| 2758 | afterPollFetch.isFirstCall = true; |
| 2759 | |
| 2760 | /** |
| 2761 | Initiates, if it's not already running, a single long-poll |
| 2762 | request to the /chat-poll endpoint. In the handling of that |
| 2763 | response, it end up will psuedo-recursively calling itself via |
| 2764 | the response-handling process. Despite being async, the implied |
| 2765 | returned Promise is meaningless. |
| 2766 | */ |
| 2767 | const poll = Chat.poll = async function f(){ |
| 2768 | if(f.running) return; |
| 2769 | f.running = true; |
| 2770 | Chat._isBatchLoading = f.isFirstCall; |
| 2771 | if(true===f.isFirstCall){ |
| 2772 | f.isFirstCall = false; |
| 2773 | f.pendingOnError = undefined; |
| 2774 | Chat.ajaxStart(); |
| 2775 | Chat.e.viewMessages.classList.add('loading'); |
| 2776 | /* |
| 2777 | We manager onerror() results in poll() in a roundabout |
| 2778 | manner: when an onerror() arrives, we stash it aside |
| @@ -2757,18 +2793,17 @@ | |
| 2793 | this one is a lot less explicable. (It's almost certainly a |
| 2794 | mis-handling bug in F.fetch(), but it has so far eluded my |
| 2795 | eyes.) |
| 2796 | */ |
| 2797 | f.delayPendingOnError = function(err){ |
| 2798 | if( f.pendingOnError ){ |
| 2799 | const x = f.pendingOnError; |
| 2800 | f.pendingOnError = undefined; |
| 2801 | afterPollFetch(x); |
| 2802 | } |
| 2803 | }; |
| 2804 | } |
| 2805 | F.fetch("chat-poll",{ |
| 2806 | timeout: Chat.timer.pollTimeout, |
| 2807 | urlParams:{ |
| 2808 | name: Chat.mxMsg |
| 2809 | }, |
| @@ -2777,20 +2812,20 @@ | |
| 2812 | beforesend: chatPollBeforeSend, |
| 2813 | aftersend: function(){ |
| 2814 | poll.running = false; |
| 2815 | }, |
| 2816 | ontimeout: function(err){ |
| 2817 | f.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/; |
| 2818 | afterPollFetch(err); |
| 2819 | }, |
| 2820 | onerror:function(err){ |
| 2821 | Chat._isBatchLoading = false; |
| 2822 | if(Chat.beVerbose){ |
| 2823 | console.error("poll.onerror:",err.name,err.status,JSON.stringify(err)); |
| 2824 | } |
| 2825 | f.pendingOnError = err; |
| 2826 | setTimeout(f.delayPendingOnError, 100); |
| 2827 | }, |
| 2828 | onload:function(y){ |
| 2829 | reportConnectionOkay('poll.onload', true); |
| 2830 | newcontent(y); |
| 2831 | if(Chat._isBatchLoading){ |
| @@ -2804,12 +2839,12 @@ | |
| 2839 | poll.isFirstCall = true; |
| 2840 | Chat._gotServerError = poll.running = false; |
| 2841 | if( window.fossil.config.chat.fromcli ){ |
| 2842 | Chat.chatOnlyMode(true); |
| 2843 | } |
| 2844 | Chat.timer.startPendingPollTimer(); |
| 2845 | delete ForceResizeKludge.$disabled; |
| 2846 | ForceResizeKludge(); |
| 2847 | Chat.animate.$disabled = false; |
| 2848 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2849 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2850 | }); |
| 2851 |