Fossil

Check-in [e8bbaf924f]
Login

Check-in [e8bbaf924f]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Further refinements of the chat poll connection detection. The first N ignored errors are now spaced out unevenly. Use the server's configured chat-poll-timeout as the basis for calculating our client-side timeout time.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: e8bbaf924f97764e3effd88f4d16dd79b381dc855deba5ecc5bf454dbfad7c63
User & Date: stephan 2025-04-11 18:52:49.044
Context
2025-04-11
19:35
Replace an a recurrent setInterval() timer in /chat's poll-connection error handler with a single-fire-as-needed setTimeout(). This saves some CPU and allows /chat to respond more quickly to non-timeout HTTP errors. ... (check-in: 1bfb06c752 user: stephan tags: trunk)
18:52
Further refinements of the chat poll connection detection. The first N ignored errors are now spaced out unevenly. Use the server's configured chat-poll-timeout as the basis for calculating our client-side timeout time. ... (check-in: e8bbaf924f user: stephan tags: trunk)
16:09
Minor cosmetic tweaks to the poll-in-distress indicator. Make it yellow in dark-mode skins, as red blends in too well. No functional changes. ... (check-in: 160d26923b user: stephan tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
252
253
254
255
256
257
258
259

260
261
262
263
264
265
266
  @ window.addEventListener('load', function(){
  @ document.body.classList.add('chat');
  @ /*^^^for skins which add their own BODY tag */;
  @ window.fossil.config.chat = {
  @   fromcli: %h(PB("cli")?"true":"false"),
  @   alertSound: "%h(zAlert)",
  @   initSize: %d(db_get_int("chat-initial-history",50)),
  @   imagesInline: !!%d(db_get_boolean("chat-inline-images",1))

  @ };
  ajax_emit_js_preview_modes(0);
  chat_emit_alert_list();
  @ }, false);
  @ </script>
  builtin_request_js("fossil.page.chat.js");
  style_finish_page();







|
>







252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
  @ window.addEventListener('load', function(){
  @ document.body.classList.add('chat');
  @ /*^^^for skins which add their own BODY tag */;
  @ window.fossil.config.chat = {
  @   fromcli: %h(PB("cli")?"true":"false"),
  @   alertSound: "%h(zAlert)",
  @   initSize: %d(db_get_int("chat-initial-history",50)),
  @   imagesInline: !!%d(db_get_boolean("chat-inline-images",1)),
  @   pollTimeout: %d(db_get_int("chat-poll-timeout",420))
  @ };
  ajax_emit_js_preview_modes(0);
  chat_emit_alert_list();
  @ }, false);
  @ </script>
  builtin_request_js("fossil.page.chat.js");
  style_finish_page();
Changes to src/fossil.fetch.js.
33
34
35
36
37
38
39
40
41
42
43
44
45












46
47
48
49
50
51
52
   error or timeout while awaiting a response, or if the onload()
   handler throws an exception. In the context of the callback, the
   options object is "this". Note that this function is intended to be
   used solely for error reporting, not error recovery. Because
   onerror() may be called if onload() throws, it is up to the caller
   to ensure that their onerror() callback references only state which
   is valid in such a case. Special cases for the Error object: (1) If
   the connection times out, the error object will have its
   (.name='timeout') and its (.status=XHR.status) set. (2) If it gets
   a non 2xx HTTP code then it will have
   (.name='http',.status=XHR.status). (3) If it was proxied through a
   JSON-format exception on the server, it will have
   (.name='json',status=XHR.status).













   - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!

   - payload: anything acceptable by XHR2.send(ARG) (DOMString,
   Document, FormData, Blob, File, ArrayBuffer), or a plain object or
   array, either of which gets JSON.stringify()'d. If payload is set
   then the method is automatically set to 'POST'. By default XHR2







|
|
|



>
>
>
>
>
>
>
>
>
>
>
>







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
   error or timeout while awaiting a response, or if the onload()
   handler throws an exception. In the context of the callback, the
   options object is "this". Note that this function is intended to be
   used solely for error reporting, not error recovery. Because
   onerror() may be called if onload() throws, it is up to the caller
   to ensure that their onerror() callback references only state which
   is valid in such a case. Special cases for the Error object: (1) If
   the connection times out via XHR.ontimeout(), the error object will
   have its (.name='timeout', .status=XHR.status) set. (2) Else if it
   gets a non 2xx HTTP code then it will have
   (.name='http',.status=XHR.status). (3) If it was proxied through a
   JSON-format exception on the server, it will have
   (.name='json',status=XHR.status).

   - ontimeout: callback(Error object). If set, timeout errors are
   reported here, else they are reported through onerror().
   Unfortunately, XHR fires two events for a timeout: an
   onreadystatechange() and an ontimeout(), in that order.  From the
   former, however, we cannot unambiguously identify the error as
   having been caused by a timeout, so clients which set ontimeout()
   will get _two_ callback calls: one with noting HTTP 0 response
   followed immediately by an ontimeout() response. Error objects
   thown passed to this will have (.name='timeout') and
   (.status=xhr.HttpStatus).  In the context of the callback, the
   options object is "this",

   - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!

   - payload: anything acceptable by XHR2.send(ARG) (DOMString,
   Document, FormData, Blob, File, ArrayBuffer), or a plain object or
   array, either of which gets JSON.stringify()'d. If payload is set
   then the method is automatically set to 'POST'. By default XHR2
168
169
170
171
172
173
174
175
176
177
178
179

180
181
182

183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

202
203
204
205
206
207
208
209
210




211
212
213
214
215
216
217
       list. We use it as a flag to tell us to JSON.parse()
       the response. */
    jsonResponse = true;
    x.responseType = 'text';
  }else{
    x.responseType = opt.responseType||'text';
  }
  x.ontimeout = function(){
    try{opt.aftersend()}catch(e){/*ignore*/}
    const err = new Error("XHR timeout of "+x.timeout+"ms expired.");
    err.status = x.status;
    err.name = 'timeout';

    opt.onerror(err);
  };
  x.onreadystatechange = function(){

    if(XMLHttpRequest.DONE !== x.readyState) return;
    try{opt.aftersend()}catch(e){/*ignore*/}
    if(false && 0===x.status){
      /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
         when the /chat page starts up, but not in Chrome nor in other
         apps. Insofar as has been determined, this happens before a
         request is actually sent and it appears to have no
         side-effects on the app other than to generate an error
         (i.e. no requests/responses are missing). This is a silly
         workaround which may or may not bite us later. If so, it can
         be removed at the cost of an unsightly console error message
         in FF.

         2025-04-10: that behavior is now also in Chrome and enabling
         this workaround causes our timeout errors to never arrive.
      */
      return;
    }
    if(200!==x.status){

      let err;
      try{
        const j = JSON.parse(x.response);
        if(j.error){
          err = new Error(j.error);
          err.name = 'json.error';
        }
      }catch(ex){/*ignore*/}
      if( !err ){




        err = new Error("HTTP response status "+x.status+".")
        err.name = 'http';
      }
      err.status = x.status;
      opt.onerror(err);
      return;
    }







|




>
|

|
>



















>









>
>
>
>







180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
       list. We use it as a flag to tell us to JSON.parse()
       the response. */
    jsonResponse = true;
    x.responseType = 'text';
  }else{
    x.responseType = opt.responseType||'text';
  }
  x.ontimeout = function(ev){
    try{opt.aftersend()}catch(e){/*ignore*/}
    const err = new Error("XHR timeout of "+x.timeout+"ms expired.");
    err.status = x.status;
    err.name = 'timeout';
    //console.warn("fetch.ontimeout",ev);
    (opt.ontimeout || opt.onerror)(err);
  };
  x.onreadystatechange = function(ev){
    //console.warn("onreadystatechange", ev.target);
    if(XMLHttpRequest.DONE !== x.readyState) return;
    try{opt.aftersend()}catch(e){/*ignore*/}
    if(false && 0===x.status){
      /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
         when the /chat page starts up, but not in Chrome nor in other
         apps. Insofar as has been determined, this happens before a
         request is actually sent and it appears to have no
         side-effects on the app other than to generate an error
         (i.e. no requests/responses are missing). This is a silly
         workaround which may or may not bite us later. If so, it can
         be removed at the cost of an unsightly console error message
         in FF.

         2025-04-10: that behavior is now also in Chrome and enabling
         this workaround causes our timeout errors to never arrive.
      */
      return;
    }
    if(200!==x.status){
      //console.warn("Error response",ev.target);
      let err;
      try{
        const j = JSON.parse(x.response);
        if(j.error){
          err = new Error(j.error);
          err.name = 'json.error';
        }
      }catch(ex){/*ignore*/}
      if( !err ){
        /* We can't tell from here whether this was a timeout-capable
           request which timed out on our end or was one which is a
           genuine error. We also don't know whether the server timed
           out the connection before we did. */
        err = new Error("HTTP response status "+x.status+".")
        err.name = 'http';
      }
      err.status = x.status;
      opt.onerror(err);
      return;
    }
241
242
243
244
245
246
247

248
249
250
251
252
253
254
    opt.onerror(e);
    return;
  }
  x.open(opt.method||'GET', url.join(''), true);
  if('POST'===opt.method && 'string'===typeof opt.contentType){
    x.setRequestHeader('Content-Type',opt.contentType);
  }

  x.timeout = +opt.timeout || f.timeout;
  if(undefined!==payload) x.send(payload);
  else x.send();
  return this;
};

/**







>







260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
    opt.onerror(e);
    return;
  }
  x.open(opt.method||'GET', url.join(''), true);
  if('POST'===opt.method && 'string'===typeof opt.contentType){
    x.setRequestHeader('Content-Type',opt.contentType);
  }
  x.hasExplicitTimeout = !!(+opt.timeout);
  x.timeout = +opt.timeout || f.timeout;
  if(undefined!==payload) x.send(payload);
  else x.send();
  return this;
};

/**
Changes to src/fossil.page.chat.js.
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207


208
209
210














211
212
213

214
215
216
217
218
219
220
221
222
223

224
225
226

227
228
229
230
231
232
233
      /**
         The timer object is used to control connection throttling
         when connection errors arrise. It starts off with a polling
         delay of $initialDelay ms. If there's a connection error,
         that gets bumped by some value for each subsequent error, up
         to some max value.

         The timeing of resetting the delay when service returns is,
         because of the long-poll connection and our lack of low-level
         insight into the connection at this level, a bit wonky.
      */
      timer:{
        tidPoller: undefined /* poller timer */,
        $initialDelay: 1000 /* initial polling interval (ms) */,
        currentDelay: 1000 /* current polling interval */,
        maxDelay: 60000 * 5 /* max interval when backing off for
                              connection errors */,
        minDelay: 5000 /* minimum delay time */,
        tidReconnect: undefined /*timer id for reconnection determination*/,


        errCount: 0 /* Current poller connection error count */,
        minErrForNotify: 4 /* Don't warn for connection errors until this
                              many have occurred */,














        randomInterval: function(factor){
          return Math.floor(Math.random() * factor);
        },

        incrDelay: function(){
          if( this.maxDelay > this.currentDelay ){
            if(this.currentDelay < this.minDelay){
              this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
            }else{
              this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
            }
          }
          return this.currentDelay;
        },

        resetDelay: function(ms){
          return this.currentDelay = ms || this.$initialDelay;
        },

        isDelayed: function(){
          return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
        }
      },
      /**
         Gets (no args) or sets (1 arg) the current input text field
         value, taking into account single- vs multi-line input. The







|




|





|
>
>



>
>
>
>
>
>
>
>
>
>
>
>
>
>



>










>
|


>







189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
      /**
         The timer object is used to control connection throttling
         when connection errors arrise. It starts off with a polling
         delay of $initialDelay ms. If there's a connection error,
         that gets bumped by some value for each subsequent error, up
         to some max value.

         The timing of resetting the delay when service returns is,
         because of the long-poll connection and our lack of low-level
         insight into the connection at this level, a bit wonky.
      */
      timer:{
        tidPoller: undefined /* setTimeout() poller timer id */,
        $initialDelay: 1000 /* initial polling interval (ms) */,
        currentDelay: 1000 /* current polling interval */,
        maxDelay: 60000 * 5 /* max interval when backing off for
                              connection errors */,
        minDelay: 5000 /* minimum delay time */,
        tidReconnect: undefined /*setTimeout() timer id for
                                  reconnection determination. See
                                  clearPollErrOnWait(). */,
        errCount: 0 /* Current poller connection error count */,
        minErrForNotify: 4 /* Don't warn for connection errors until this
                              many have occurred */,
        pollTimeout: (1 && window.location.hostname.match(
          "localhost" /*presumably local dev mode*/
        )) ? 15000
          : (+F.config.chat.pollTimeout>0
             ? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1)))
             /* ^^^^^^^^^^^^ we want our timeouts to be slightly shorter
                than the server's so that we can distingished timed-out
                polls on our end from HTTP errors (if the server times
                out). */
             : 30000),
        /** Returns a random fudge value for reconnect attempt times,
            intended to keep the /chat server from getting hammered if
            all clients which were just disconnected all reconnect at
            the same instant. */
        randomInterval: function(factor){
          return Math.floor(Math.random() * factor);
        },
        /** Increments the reconnection delay, within some min/max range. */
        incrDelay: function(){
          if( this.maxDelay > this.currentDelay ){
            if(this.currentDelay < this.minDelay){
              this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
            }else{
              this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
            }
          }
          return this.currentDelay;
        },
        /** Resets the delay counter to v || its initial value. */
        resetDelay: function(ms=0){
          return this.currentDelay = ms || this.$initialDelay;
        },
        /** Returns true if the timer is set to delayed mode. */
        isDelayed: function(){
          return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
        }
      },
      /**
         Gets (no args) or sets (1 arg) the current input text field
         value, taking into account single- vs multi-line input. The
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
              msgid: "reconnect-"+(++InternalMsgId),
              mtime: d,
              lmtime: d,
              xmsg: args
            });
      this.injectMessageElem(mw.e.body);
      mw.scrollIntoView();
      //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction
      return mw;
    };

    cs.getMessageElemById = function(id){
      return qs('[data-msgid="'+id+'"]');
    };








<







756
757
758
759
760
761
762

763
764
765
766
767
768
769
              msgid: "reconnect-"+(++InternalMsgId),
              mtime: d,
              lmtime: d,
              xmsg: args
            });
      this.injectMessageElem(mw.e.body);
      mw.scrollIntoView();

      return mw;
    };

    cs.getMessageElemById = function(id){
      return qs('[data-msgid="'+id+'"]');
    };

863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
      }
      // We need to fetch the plain-text version...
      const self = this;
      F.fetch('chat-fetch-one',{
        urlParams:{ name: id, raw: true},
        responseType: 'json',
        onload: function(msg){
          reportConnectionReestablished('chat-fetch-one');
          content.$elems[1] = D.append(D.pre(),msg.xmsg);
          content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
          self.toggleTextMode(e);
        },
        aftersend:function(){
          delete e.$isToggling;
          Chat.ajaxEnd();







|







881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
      }
      // We need to fetch the plain-text version...
      const self = this;
      F.fetch('chat-fetch-one',{
        urlParams:{ name: id, raw: true},
        responseType: 'json',
        onload: function(msg){
          reportConnectionOkay('chat-fetch-one');
          content.$elems[1] = D.append(D.pre(),msg.xmsg);
          content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
          self.toggleTextMode(e);
        },
        aftersend:function(){
          delete e.$isToggling;
          Chat.ajaxEnd();
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
      }
      if(!(e instanceof HTMLElement)) return;
      if(this.userMayDelete(e)){
        this.ajaxStart();
        F.fetch("chat-delete/" + id, {
          responseType: 'json',
          onload:(r)=>{
            reportConnectionReestablished('chat-delete');
            this.deleteMessageElem(r);
          },
          onerror:(err)=>this.reportErrorAsMessage(err)
        });
      }else{
        this.deleteMessageElem(id);
      }







|







944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
      }
      if(!(e instanceof HTMLElement)) return;
      if(this.userMayDelete(e)){
        this.ajaxStart();
        F.fetch("chat-delete/" + id, {
          responseType: 'json',
          onload:(r)=>{
            reportConnectionOkay('chat-delete');
            this.deleteMessageElem(r);
          },
          onerror:(err)=>this.reportErrorAsMessage(err)
        });
      }else{
        this.deleteMessageElem(id);
      }
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
          urlParams:{
            q: '',
            n: nFetch,
            i: iFirst
          },
          responseType: "json",
          onload:function(jx){
            reportConnectionReestablished('chat-query.onload');
            if( bDown ) jx.msgs.reverse();
            jx.msgs.forEach((m) => {
              m.isSearchResult = true;
              var mw = new Chat.MessageWidget(m);
              if( bDown ){
                /* Inject the message below this object's body, or
                   append it to Chat.e.searchContent if this element







|







1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
          urlParams:{
            q: '',
            n: nFetch,
            i: iFirst
          },
          responseType: "json",
          onload:function(jx){
            reportConnectionOkay('chat-query');
            if( bDown ) jx.msgs.reverse();
            jx.msgs.forEach((m) => {
              m.isSearchResult = true;
              var mw = new Chat.MessageWidget(m);
              if( bDown ){
                /* Inject the message below this object's body, or
                   append it to Chat.e.searchContent if this element
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745

1746
1747
1748
1749
1750
1751
1752
    }));
    Chat.reportErrorAsMessage(w);
  };

  /* Assume the connection has been established, reset the
     Chat.timer.tidReconnect, and (if showMsg and
     !!Chat.e.eMsgPollError) alert the user that the outage appears to
     be over. Then schedule Chat.poll() to run in the very near
     future. */
  const reportConnectionReestablished = function(dbgContext, showMsg = true){
    if(Chat.beVerbose){
      console.warn('reportConnectionReestablished', dbgContext,
                   'Chat.e.pollErrorMarker =',Chat.e.pollErrorMarker,
                   'Chat.timer.tidReconnect =',Chat.timer.tidReconnect,
                   'Chat.timer =',Chat.timer);
    }

    if( Chat.timer.errCount ){
      D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
      Chat.timer.errCount = 0;
    }
    if( Chat.timer.tidReconnect ){
      clearTimeout(Chat.timer.tidReconnect);
      Chat.timer.tidReconnect = 0;







|

|

|




>







1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
    }));
    Chat.reportErrorAsMessage(w);
  };

  /* Assume the connection has been established, reset the
     Chat.timer.tidReconnect, and (if showMsg and
     !!Chat.e.eMsgPollError) alert the user that the outage appears to
     be over. Also schedule Chat.poll() to run in the very near
     future. */
  const reportConnectionOkay = function(dbgContext, showMsg = true){
    if(Chat.beVerbose){
      console.warn('reportConnectionOkay', dbgContext,
                   'Chat.e.pollErrorMarker =',Chat.e.pollErrorMarker,
                   'Chat.timer.tidReconnect =',Chat.timer.tidReconnect,
                   'Chat.timer =',Chat.timer);
    }
    setTimeout( Chat.poll, Chat.timer.resetDelay() );
    if( Chat.timer.errCount ){
      D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
      Chat.timer.errCount = 0;
    }
    if( Chat.timer.tidReconnect ){
      clearTimeout(Chat.timer.tidReconnect);
      Chat.timer.tidReconnect = 0;
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
        if( oldErrMsg ){
          D.remove(oldErrMsg.e?.body.querySelector('button.retry-now'));
        }
        m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
        D.addClass(m.e.body,'poller-connection');
      }
    }
    setTimeout( Chat.poll, Chat.timer.resetDelay() );
  };

  /* To be called from F.fetch('chat-poll') beforesend() handler.  If
     we're currently in delayed-retry mode and a connection is
     started, try to reset the delay after N time waiting on that
     connection. The fact that the connection is waiting to respond,
     rather than outright failing, is a good hint that the outage is
     over and we can reset the back-off timer. */
  const clearPollErrOnWait = function(){
    //console.warn('clearPollErrOnWait outer', Chat.timer.tidReconnect, Chat.timer.currentDelay);
    if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){
      Chat.timer.tidReconnect = setTimeout(()=>{
        //console.warn('clearPollErrOnWait inner');
        Chat.timer.tidReconnect = 0;
        if( poll.running ){
          /* This chat-poll F.fetch() is still underway, so let's
             assume the connection is back up until/unless it times
             out or breaks again. */
          reportConnectionReestablished('clearPollErrOnWait');
        }
      }, Chat.timer.$initialDelay * 3 );
    }
  };

  /**
     Submits the contents of the message input field (if not empty)
     and/or the file attachment field to the server. If both are
     empty, this is a no-op.








<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







1781
1782
1783
1784
1785
1786
1787























1788
1789
1790
1791
1792
1793
1794
        if( oldErrMsg ){
          D.remove(oldErrMsg.e?.body.querySelector('button.retry-now'));
        }
        m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
        D.addClass(m.e.body,'poller-connection');
      }
    }























  };

  /**
     Submits the contents of the message input field (if not empty)
     and/or the file attachment field to the server. If both are
     empty, this is a no-op.

1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
      payload: fd,
      responseType: 'text',
      onerror:function(err){
        self.reportErrorAsMessage(err);
        recoverFailedMessage(fallback);
      },
      onload:function(txt){
        reportConnectionReestablished();
        if(!txt) return/*success response*/;
        try{
          const json = JSON.parse(txt);
          self.newContent({msgs:[json]});
        }catch(e){
          self.reportError(e);
        }







|







1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
      payload: fd,
      responseType: 'text',
      onerror:function(err){
        self.reportErrorAsMessage(err);
        recoverFailedMessage(fallback);
      },
      onload:function(txt){
        reportConnectionOkay('chat-send');
        if(!txt) return/*success response*/;
        try{
          const json = JSON.parse(txt);
          self.newContent({msgs:[json]});
        }catch(e){
          self.reportError(e);
        }
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
      fd.append('content', txt);
      fd.append('filename','chat.md'
                /*filename needed for mimetype determination*/);
      fd.append('render_mode',F.page.previewModes.wiki);
      F.fetch('ajax/preview-text',{
        payload: fd,
        onload: function(html){
          reportConnectionReestablished();
          Chat.setPreviewText(html);
          F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
        },
        onerror: function(e){
          F.fetch.onerror(e);
          Chat.setPreviewText("ERROR: "+(
            e.message || 'Unknown error fetching preview!'







|







2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
      fd.append('content', txt);
      fd.append('filename','chat.md'
                /*filename needed for mimetype determination*/);
      fd.append('render_mode',F.page.previewModes.wiki);
      F.fetch('ajax/preview-text',{
        payload: fd,
        onload: function(html){
          reportConnectionOkay('ajax/preview-text');
          Chat.setPreviewText(html);
          F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
        },
        onerror: function(e){
          F.fetch.onerror(e);
          Chat.setPreviewText("ERROR: "+(
            e.message || 'Unknown error fetching preview!'
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
        },
        responseType: 'json',
        onerror:function(err){
          Chat.reportErrorAsMessage(err);
          Chat._isBatchLoading = false;
        },
        onload:function(x){
          reportConnectionReestablished();
          let gotMessages = x.msgs.length;
          newcontent(x,true);
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
          if(Chat._gotServerError){
            Chat._gotServerError = false;
            return;







|







2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
        },
        responseType: 'json',
        onerror:function(err){
          Chat.reportErrorAsMessage(err);
          Chat._isBatchLoading = false;
        },
        onload:function(x){
          reportConnectionOkay('loadOldMessages()');
          let gotMessages = x.msgs.length;
          newcontent(x,true);
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
          if(Chat._gotServerError){
            Chat._gotServerError = false;
            return;
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
        payload: fd,
        responseType: 'json',
        onerror:function(err){
          Chat.setCurrentView(Chat.e.viewMessages);
          Chat.reportErrorAsMessage(err);
        },
        onload:function(jx){
          reportConnectionReestablished();
          let previd = 0;
          D.clearElement(eMsgTgt);
          jx.msgs.forEach((m)=>{
            m.isSearchResult = true;
            const mw = new Chat.MessageWidget(m);
            const spacer = new Chat.SearchCtxLoader({
              first: jx.first,







|







2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
        payload: fd,
        responseType: 'json',
        onerror:function(err){
          Chat.setCurrentView(Chat.e.viewMessages);
          Chat.reportErrorAsMessage(err);
        },
        onload:function(jx){
          reportConnectionOkay('submitSearch()');
          let previd = 0;
          D.clearElement(eMsgTgt);
          jx.msgs.forEach((m)=>{
            m.isSearchResult = true;
            const mw = new Chat.MessageWidget(m);
            const spacer = new Chat.SearchCtxLoader({
              first: jx.first,
2609
2610
2611
2612
2613
2614
2615





























2616
2617
2618
2619
2620
2621
2622
                      'No search results found for: ',
                      term );
          }
        }
      }
    );
  }/*Chat.submitSearch()*/;






























  /**
     Deal with the last poll() response and maybe re-start poll().
  */
  const afterPollFetch = function f(err){
    if(true===f.isFirstCall){
      f.isFirstCall = false;







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
                      'No search results found for: ',
                      term );
          }
        }
      }
    );
  }/*Chat.submitSearch()*/;

  /* To be called from F.fetch('chat-poll') beforesend() handler.  If
     we're currently in delayed-retry mode and a connection is
     started, try to reset the delay after N time waiting on that
     connection. The fact that the connection is waiting to respond,
     rather than outright failing, is a good hint that the outage is
     over and we can reset the back-off timer.

     Without this, recovery of a connection error won't be reported
     until after the long-poll completes by either receiving new
     messages or times out. Once a long-poll is in progress, though,
     we "know" that it's up and running again, so can update the UI
     and connection timer to reflect that.
  */
  const chatPollBeforeSend = function(){
    //console.warn('chatPollBeforeSend outer', Chat.timer.tidReconnect, Chat.timer.currentDelay);
    if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){
      Chat.timer.tidReconnect = setTimeout(()=>{
        //console.warn('chatPollBeforeSend inner');
        Chat.timer.tidReconnect = 0;
        if( poll.running ){
          /* This chat-poll F.fetch() is still underway, so let's
             assume the connection is back up until/unless it times
             out or breaks again. */
          reportConnectionOkay('chatPollBeforeSend', true);
        }
      }, Chat.timer.$initialDelay * 3 );
    }
  };

  /**
     Deal with the last poll() response and maybe re-start poll().
  */
  const afterPollFetch = function f(err){
    if(true===f.isFirstCall){
      f.isFirstCall = false;
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653




2654
2655

2656
2657
2658
2659
2660
2661
2662
2663
2664
      Chat.timer.tidPoller = undefined;
    } else {
      if( err && Chat.beVerbose ){
        console.error("afterPollFetch:",err.name,err.status,err.message);
      }
      if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){
        /* Restart the poller immediately. */
        reportConnectionReestablished('afterPollFetch '+err, false);
      }else{
        /* Delay a while before trying again, noting that other Chat
           APIs may try and succeed at connections before this timer
           resolves, in which case they'll clear this timeout and the
           UI message about the outage. */
        let delay;
        D.addClass(Chat.e.pollErrorMarker, 'connection-error');
        if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){




          if(Chat.beVerbose){
            console.warn("Ignoring polling error #", Chat.timer.errCount);

          }
          delay = Chat.timer.resetDelay(Chat.timer.minDelay);
        } else {
          delay = Chat.timer.incrDelay();
          //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError);
          const msg = "Poller connection error. Retrying in "+delay+ " ms.";
          /* Replace the current/newest connection error widget. We could also
             just update its body with the new message, but then its timestamp
             never updates. OTOH, if we replace the message, we lose the







|








>
>
>
>

|
>

<







2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686

2687
2688
2689
2690
2691
2692
2693
      Chat.timer.tidPoller = undefined;
    } else {
      if( err && Chat.beVerbose ){
        console.error("afterPollFetch:",err.name,err.status,err.message);
      }
      if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){
        /* Restart the poller immediately. */
        reportConnectionOkay('afterPollFetch '+err, false);
      }else{
        /* Delay a while before trying again, noting that other Chat
           APIs may try and succeed at connections before this timer
           resolves, in which case they'll clear this timeout and the
           UI message about the outage. */
        let delay;
        D.addClass(Chat.e.pollErrorMarker, 'connection-error');
        if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){
          delay = Chat.timer.resetDelay(
            (Chat.timer.minDelay * Chat.timer.errCount)
              + Chat.timer.randomInterval(Chat.timer.minDelay)
          );
          if(Chat.beVerbose){
            console.warn("Ignoring polling error #",Chat.timer.errCount,
                         "for another",delay,"ms" );
          }

        } else {
          delay = Chat.timer.incrDelay();
          //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError);
          const msg = "Poller connection error. Retrying in "+delay+ " ms.";
          /* Replace the current/newest connection error widget. We could also
             just update its body with the new message, but then its timestamp
             never updates. OTOH, if we replace the message, we lose the
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715

2716
2717
2718


2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761




2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
  */
  const poll = Chat.poll = async function f(){
    if(f.running) return;
    f.running = true;
    Chat._isBatchLoading = f.isFirstCall;
    if(true===f.isFirstCall){
      f.isFirstCall = false;
      Chat.aPollErr = [];
      Chat.ajaxStart();
      Chat.e.viewMessages.classList.add('loading');
      setInterval(
        /*
          We manager onerror() results in poll() using a
          stack of error objects and we delay their handling by
          a small amount, rather than immediately when the
          exception arrives.

          This level of indirection is to work around an inexplicable

          behavior from the F.fetch() connections: timeouts are always
          announced in pairs of an HTTP 0 and something we can
          unambiguously identify as a timeout. When that happens, we


          ignore the HTTP 0. If, however, an HTTP 0 is seen here
          without an immediately-following timeout, we process
          it. Attempts to squelch the HTTP 0 response at their source,
          in F.fetch(), have led to worse breakage.

          It's kinda like in the curses C API, where you to match
          ALT-X by first getting an ESC event, then an X event, but
          this one is a lot less explicable. (It's almost certainly a
          mis-handling bug in F.fetch(), but it has so far eluded my
          eyes.)
        */
        ()=>{
          if( Chat.aPollErr.length ){
            if(Chat.aPollErr.length>1){
              //console.warn('aPollErr',Chat.aPollErr);
              if(Chat.aPollErr[1].name==='timeout'){
                /* mysterious pairs of HTTP 0 followed immediately
                   by timeout response; ignore the former in that case. */
                Chat.aPollErr.shift();
              }
            }
            afterPollFetch(Chat.aPollErr.shift());
          }
        },
        1000
      );
    }
    let nErr = 0;
    F.fetch("chat-poll",{
      timeout: window.location.hostname.match(
        "localhost" /*presumably local dev mode*/
      ) ? 15000
        : 420 * 1000/*FIXME: get the value from the server*/,
      urlParams:{
        name: Chat.mxMsg
      },
      responseType: "json",
      // Disable the ajax start/end handling for this long-polling op:
      beforesend: function(){
        clearPollErrOnWait();
      },
      aftersend: function(){
        poll.running = false;




      },
      onerror:function(err){
        Chat._isBatchLoading = false;
        if(Chat.beVerbose){
          console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
        }
        Chat.aPollErr.push(err);
      },
      onload:function(y){
        reportConnectionReestablished('poll.onload', true);
        newcontent(y);
        if(Chat._isBatchLoading){
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
        }
        afterPollFetch();
      }







|


|






|
>
|
|
|
>
>
|
|
<
<








|
|
<
<
<
<
|
<
<
|







|
<
<
<





|
<
<


>
>
>
>






|


|







2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752


2753
2754
2755
2756
2757
2758
2759
2760
2761
2762




2763


2764
2765
2766
2767
2768
2769
2770
2771
2772



2773
2774
2775
2776
2777
2778


2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
  */
  const poll = Chat.poll = async function f(){
    if(f.running) return;
    f.running = true;
    Chat._isBatchLoading = f.isFirstCall;
    if(true===f.isFirstCall){
      f.isFirstCall = false;
      Chat.pendingOnError = undefined;
      Chat.ajaxStart();
      Chat.e.viewMessages.classList.add('loading');
      if(1) setInterval(
        /*
          We manager onerror() results in poll() using a
          stack of error objects and we delay their handling by
          a small amount, rather than immediately when the
          exception arrives.

          This level of indirection is necessary to be able to
          unambiguously identify client-timeout-specific polling
          errors from other errors. Timeouts are always announced in
          pairs of an HTTP 0 and something we can unambiguously
          identify as a timeout. When we receive an HTTP 0 we put it
          into this queue. If an ontimeout() call arrives before this
          error is handled, this error is removed from the stack.  If,
          however, an HTTP 0 is seen in this stack without an
          accompanying timeout, we handle it from here.



          It's kinda like in the curses C API, where you to match
          ALT-X by first getting an ESC event, then an X event, but
          this one is a lot less explicable. (It's almost certainly a
          mis-handling bug in F.fetch(), but it has so far eluded my
          eyes.)
        */
        ()=>{
          if( Chat.pendingOnError ){
            const x = Chat.pendingOnError;




            Chat.pendingOnError = undefined;


            afterPollFetch(x);
          }
        },
        1000
      );
    }
    let nErr = 0;
    F.fetch("chat-poll",{
      timeout: Chat.timer.pollTimeout,



      urlParams:{
        name: Chat.mxMsg
      },
      responseType: "json",
      // Disable the ajax start/end handling for this long-polling op:
      beforesend: chatPollBeforeSend,


      aftersend: function(){
        poll.running = false;
      },
      ontimeout: function(err){
        Chat.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/;
        afterPollFetch(err);
      },
      onerror:function(err){
        Chat._isBatchLoading = false;
        if(Chat.beVerbose){
          console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
        }
        Chat.pendingOnError = err;
      },
      onload:function(y){
        reportConnectionOkay('poll.onload', true);
        newcontent(y);
        if(Chat._isBatchLoading){
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
        }
        afterPollFetch();
      }