Fossil SCM

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.

stephan 2025-04-11 18:52 trunk
Commit e8bbaf924f97764e3effd88f4d16dd79b381dc855deba5ecc5bf454dbfad7c63
+2 -1
--- src/chat.c
+++ src/chat.c
@@ -254,11 +254,12 @@
254254
@ /*^^^for skins which add their own BODY tag */;
255255
@ window.fossil.config.chat = {
256256
@ fromcli: %h(PB("cli")?"true":"false"),
257257
@ alertSound: "%h(zAlert)",
258258
@ initSize: %d(db_get_int("chat-initial-history",50)),
259
- @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
259
+ @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)),
260
+ @ pollTimeout: %d(db_get_int("chat-poll-timeout",420))
260261
@ };
261262
ajax_emit_js_preview_modes(0);
262263
chat_emit_alert_list();
263264
@ }, false);
264265
@ </script>
265266
--- src/chat.c
+++ src/chat.c
@@ -254,11 +254,12 @@
254 @ /*^^^for skins which add their own BODY tag */;
255 @ window.fossil.config.chat = {
256 @ fromcli: %h(PB("cli")?"true":"false"),
257 @ alertSound: "%h(zAlert)",
258 @ initSize: %d(db_get_int("chat-initial-history",50)),
259 @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
 
260 @ };
261 ajax_emit_js_preview_modes(0);
262 chat_emit_alert_list();
263 @ }, false);
264 @ </script>
265
--- src/chat.c
+++ src/chat.c
@@ -254,11 +254,12 @@
254 @ /*^^^for skins which add their own BODY tag */;
255 @ window.fossil.config.chat = {
256 @ fromcli: %h(PB("cli")?"true":"false"),
257 @ alertSound: "%h(zAlert)",
258 @ initSize: %d(db_get_int("chat-initial-history",50)),
259 @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)),
260 @ pollTimeout: %d(db_get_int("chat-poll-timeout",420))
261 @ };
262 ajax_emit_js_preview_modes(0);
263 chat_emit_alert_list();
264 @ }, false);
265 @ </script>
266
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -35,16 +35,28 @@
3535
options object is "this". Note that this function is intended to be
3636
used solely for error reporting, not error recovery. Because
3737
onerror() may be called if onload() throws, it is up to the caller
3838
to ensure that their onerror() callback references only state which
3939
is valid in such a case. Special cases for the Error object: (1) If
40
- the connection times out, the error object will have its
41
- (.name='timeout') and its (.status=XHR.status) set. (2) If it gets
42
- a non 2xx HTTP code then it will have
40
+ the connection times out via XHR.ontimeout(), the error object will
41
+ have its (.name='timeout', .status=XHR.status) set. (2) Else if it
42
+ gets a non 2xx HTTP code then it will have
4343
(.name='http',.status=XHR.status). (3) If it was proxied through a
4444
JSON-format exception on the server, it will have
4545
(.name='json',status=XHR.status).
46
+
47
+ - ontimeout: callback(Error object). If set, timeout errors are
48
+ reported here, else they are reported through onerror().
49
+ Unfortunately, XHR fires two events for a timeout: an
50
+ onreadystatechange() and an ontimeout(), in that order. From the
51
+ former, however, we cannot unambiguously identify the error as
52
+ having been caused by a timeout, so clients which set ontimeout()
53
+ will get _two_ callback calls: one with noting HTTP 0 response
54
+ followed immediately by an ontimeout() response. Error objects
55
+ thown passed to this will have (.name='timeout') and
56
+ (.status=xhr.HttpStatus). In the context of the callback, the
57
+ options object is "this",
4658
4759
- method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
4860
4961
- payload: anything acceptable by XHR2.send(ARG) (DOMString,
5062
Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -170,18 +182,20 @@
170182
jsonResponse = true;
171183
x.responseType = 'text';
172184
}else{
173185
x.responseType = opt.responseType||'text';
174186
}
175
- x.ontimeout = function(){
187
+ x.ontimeout = function(ev){
176188
try{opt.aftersend()}catch(e){/*ignore*/}
177189
const err = new Error("XHR timeout of "+x.timeout+"ms expired.");
178190
err.status = x.status;
179191
err.name = 'timeout';
180
- opt.onerror(err);
192
+ //console.warn("fetch.ontimeout",ev);
193
+ (opt.ontimeout || opt.onerror)(err);
181194
};
182
- x.onreadystatechange = function(){
195
+ x.onreadystatechange = function(ev){
196
+ //console.warn("onreadystatechange", ev.target);
183197
if(XMLHttpRequest.DONE !== x.readyState) return;
184198
try{opt.aftersend()}catch(e){/*ignore*/}
185199
if(false && 0===x.status){
186200
/* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
187201
when the /chat page starts up, but not in Chrome nor in other
@@ -197,19 +211,24 @@
197211
this workaround causes our timeout errors to never arrive.
198212
*/
199213
return;
200214
}
201215
if(200!==x.status){
216
+ //console.warn("Error response",ev.target);
202217
let err;
203218
try{
204219
const j = JSON.parse(x.response);
205220
if(j.error){
206221
err = new Error(j.error);
207222
err.name = 'json.error';
208223
}
209224
}catch(ex){/*ignore*/}
210225
if( !err ){
226
+ /* We can't tell from here whether this was a timeout-capable
227
+ request which timed out on our end or was one which is a
228
+ genuine error. We also don't know whether the server timed
229
+ out the connection before we did. */
211230
err = new Error("HTTP response status "+x.status+".")
212231
err.name = 'http';
213232
}
214233
err.status = x.status;
215234
opt.onerror(err);
@@ -243,10 +262,11 @@
243262
}
244263
x.open(opt.method||'GET', url.join(''), true);
245264
if('POST'===opt.method && 'string'===typeof opt.contentType){
246265
x.setRequestHeader('Content-Type',opt.contentType);
247266
}
267
+ x.hasExplicitTimeout = !!(+opt.timeout);
248268
x.timeout = +opt.timeout || f.timeout;
249269
if(undefined!==payload) x.send(payload);
250270
else x.send();
251271
return this;
252272
};
253273
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -35,16 +35,28 @@
35 options object is "this". Note that this function is intended to be
36 used solely for error reporting, not error recovery. Because
37 onerror() may be called if onload() throws, it is up to the caller
38 to ensure that their onerror() callback references only state which
39 is valid in such a case. Special cases for the Error object: (1) If
40 the connection times out, the error object will have its
41 (.name='timeout') and its (.status=XHR.status) set. (2) If it gets
42 a non 2xx HTTP code then it will have
43 (.name='http',.status=XHR.status). (3) If it was proxied through a
44 JSON-format exception on the server, it will have
45 (.name='json',status=XHR.status).
 
 
 
 
 
 
 
 
 
 
 
 
46
47 - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
48
49 - payload: anything acceptable by XHR2.send(ARG) (DOMString,
50 Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -170,18 +182,20 @@
170 jsonResponse = true;
171 x.responseType = 'text';
172 }else{
173 x.responseType = opt.responseType||'text';
174 }
175 x.ontimeout = function(){
176 try{opt.aftersend()}catch(e){/*ignore*/}
177 const err = new Error("XHR timeout of "+x.timeout+"ms expired.");
178 err.status = x.status;
179 err.name = 'timeout';
180 opt.onerror(err);
 
181 };
182 x.onreadystatechange = function(){
 
183 if(XMLHttpRequest.DONE !== x.readyState) return;
184 try{opt.aftersend()}catch(e){/*ignore*/}
185 if(false && 0===x.status){
186 /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
187 when the /chat page starts up, but not in Chrome nor in other
@@ -197,19 +211,24 @@
197 this workaround causes our timeout errors to never arrive.
198 */
199 return;
200 }
201 if(200!==x.status){
 
202 let err;
203 try{
204 const j = JSON.parse(x.response);
205 if(j.error){
206 err = new Error(j.error);
207 err.name = 'json.error';
208 }
209 }catch(ex){/*ignore*/}
210 if( !err ){
 
 
 
 
211 err = new Error("HTTP response status "+x.status+".")
212 err.name = 'http';
213 }
214 err.status = x.status;
215 opt.onerror(err);
@@ -243,10 +262,11 @@
243 }
244 x.open(opt.method||'GET', url.join(''), true);
245 if('POST'===opt.method && 'string'===typeof opt.contentType){
246 x.setRequestHeader('Content-Type',opt.contentType);
247 }
 
248 x.timeout = +opt.timeout || f.timeout;
249 if(undefined!==payload) x.send(payload);
250 else x.send();
251 return this;
252 };
253
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -35,16 +35,28 @@
35 options object is "this". Note that this function is intended to be
36 used solely for error reporting, not error recovery. Because
37 onerror() may be called if onload() throws, it is up to the caller
38 to ensure that their onerror() callback references only state which
39 is valid in such a case. Special cases for the Error object: (1) If
40 the connection times out via XHR.ontimeout(), the error object will
41 have its (.name='timeout', .status=XHR.status) set. (2) Else if it
42 gets a non 2xx HTTP code then it will have
43 (.name='http',.status=XHR.status). (3) If it was proxied through a
44 JSON-format exception on the server, it will have
45 (.name='json',status=XHR.status).
46
47 - ontimeout: callback(Error object). If set, timeout errors are
48 reported here, else they are reported through onerror().
49 Unfortunately, XHR fires two events for a timeout: an
50 onreadystatechange() and an ontimeout(), in that order. From the
51 former, however, we cannot unambiguously identify the error as
52 having been caused by a timeout, so clients which set ontimeout()
53 will get _two_ callback calls: one with noting HTTP 0 response
54 followed immediately by an ontimeout() response. Error objects
55 thown passed to this will have (.name='timeout') and
56 (.status=xhr.HttpStatus). In the context of the callback, the
57 options object is "this",
58
59 - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
60
61 - payload: anything acceptable by XHR2.send(ARG) (DOMString,
62 Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -170,18 +182,20 @@
182 jsonResponse = true;
183 x.responseType = 'text';
184 }else{
185 x.responseType = opt.responseType||'text';
186 }
187 x.ontimeout = function(ev){
188 try{opt.aftersend()}catch(e){/*ignore*/}
189 const err = new Error("XHR timeout of "+x.timeout+"ms expired.");
190 err.status = x.status;
191 err.name = 'timeout';
192 //console.warn("fetch.ontimeout",ev);
193 (opt.ontimeout || opt.onerror)(err);
194 };
195 x.onreadystatechange = function(ev){
196 //console.warn("onreadystatechange", ev.target);
197 if(XMLHttpRequest.DONE !== x.readyState) return;
198 try{opt.aftersend()}catch(e){/*ignore*/}
199 if(false && 0===x.status){
200 /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
201 when the /chat page starts up, but not in Chrome nor in other
@@ -197,19 +211,24 @@
211 this workaround causes our timeout errors to never arrive.
212 */
213 return;
214 }
215 if(200!==x.status){
216 //console.warn("Error response",ev.target);
217 let err;
218 try{
219 const j = JSON.parse(x.response);
220 if(j.error){
221 err = new Error(j.error);
222 err.name = 'json.error';
223 }
224 }catch(ex){/*ignore*/}
225 if( !err ){
226 /* We can't tell from here whether this was a timeout-capable
227 request which timed out on our end or was one which is a
228 genuine error. We also don't know whether the server timed
229 out the connection before we did. */
230 err = new Error("HTTP response status "+x.status+".")
231 err.name = 'http';
232 }
233 err.status = x.status;
234 opt.onerror(err);
@@ -243,10 +262,11 @@
262 }
263 x.open(opt.method||'GET', url.join(''), true);
264 if('POST'===opt.method && 'string'===typeof opt.contentType){
265 x.setRequestHeader('Content-Type',opt.contentType);
266 }
267 x.hasExplicitTimeout = !!(+opt.timeout);
268 x.timeout = +opt.timeout || f.timeout;
269 if(undefined!==payload) x.send(payload);
270 else x.send();
271 return this;
272 };
273
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -191,28 +191,45 @@
191191
when connection errors arrise. It starts off with a polling
192192
delay of $initialDelay ms. If there's a connection error,
193193
that gets bumped by some value for each subsequent error, up
194194
to some max value.
195195
196
- The timeing of resetting the delay when service returns is,
196
+ The timing of resetting the delay when service returns is,
197197
because of the long-poll connection and our lack of low-level
198198
insight into the connection at this level, a bit wonky.
199199
*/
200200
timer:{
201
- tidPoller: undefined /* poller timer */,
201
+ tidPoller: undefined /* setTimeout() poller timer id */,
202202
$initialDelay: 1000 /* initial polling interval (ms) */,
203203
currentDelay: 1000 /* current polling interval */,
204204
maxDelay: 60000 * 5 /* max interval when backing off for
205205
connection errors */,
206206
minDelay: 5000 /* minimum delay time */,
207
- tidReconnect: undefined /*timer id for reconnection determination*/,
207
+ tidReconnect: undefined /*setTimeout() timer id for
208
+ reconnection determination. See
209
+ clearPollErrOnWait(). */,
208210
errCount: 0 /* Current poller connection error count */,
209211
minErrForNotify: 4 /* Don't warn for connection errors until this
210212
many have occurred */,
213
+ pollTimeout: (1 && window.location.hostname.match(
214
+ "localhost" /*presumably local dev mode*/
215
+ )) ? 15000
216
+ : (+F.config.chat.pollTimeout>0
217
+ ? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1)))
218
+ /* ^^^^^^^^^^^^ we want our timeouts to be slightly shorter
219
+ than the server's so that we can distingished timed-out
220
+ polls on our end from HTTP errors (if the server times
221
+ out). */
222
+ : 30000),
223
+ /** Returns a random fudge value for reconnect attempt times,
224
+ intended to keep the /chat server from getting hammered if
225
+ all clients which were just disconnected all reconnect at
226
+ the same instant. */
211227
randomInterval: function(factor){
212228
return Math.floor(Math.random() * factor);
213229
},
230
+ /** Increments the reconnection delay, within some min/max range. */
214231
incrDelay: function(){
215232
if( this.maxDelay > this.currentDelay ){
216233
if(this.currentDelay < this.minDelay){
217234
this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
218235
}else{
@@ -219,13 +236,15 @@
219236
this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
220237
}
221238
}
222239
return this.currentDelay;
223240
},
224
- resetDelay: function(ms){
241
+ /** Resets the delay counter to v || its initial value. */
242
+ resetDelay: function(ms=0){
225243
return this.currentDelay = ms || this.$initialDelay;
226244
},
245
+ /** Returns true if the timer is set to delayed mode. */
227246
isDelayed: function(){
228247
return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
229248
}
230249
},
231250
/**
@@ -739,11 +758,10 @@
739758
lmtime: d,
740759
xmsg: args
741760
});
742761
this.injectMessageElem(mw.e.body);
743762
mw.scrollIntoView();
744
- //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction
745763
return mw;
746764
};
747765
748766
cs.getMessageElemById = function(id){
749767
return qs('[data-msgid="'+id+'"]');
@@ -865,11 +883,11 @@
865883
const self = this;
866884
F.fetch('chat-fetch-one',{
867885
urlParams:{ name: id, raw: true},
868886
responseType: 'json',
869887
onload: function(msg){
870
- reportConnectionReestablished('chat-fetch-one');
888
+ reportConnectionOkay('chat-fetch-one');
871889
content.$elems[1] = D.append(D.pre(),msg.xmsg);
872890
content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
873891
self.toggleTextMode(e);
874892
},
875893
aftersend:function(){
@@ -928,11 +946,11 @@
928946
if(this.userMayDelete(e)){
929947
this.ajaxStart();
930948
F.fetch("chat-delete/" + id, {
931949
responseType: 'json',
932950
onload:(r)=>{
933
- reportConnectionReestablished('chat-delete');
951
+ reportConnectionOkay('chat-delete');
934952
this.deleteMessageElem(r);
935953
},
936954
onerror:(err)=>this.reportErrorAsMessage(err)
937955
});
938956
}else{
@@ -1560,11 +1578,11 @@
15601578
n: nFetch,
15611579
i: iFirst
15621580
},
15631581
responseType: "json",
15641582
onload:function(jx){
1565
- reportConnectionReestablished('chat-query.onload');
1583
+ reportConnectionOkay('chat-query');
15661584
if( bDown ) jx.msgs.reverse();
15671585
jx.msgs.forEach((m) => {
15681586
m.isSearchResult = true;
15691587
var mw = new Chat.MessageWidget(m);
15701588
if( bDown ){
@@ -1732,19 +1750,20 @@
17321750
};
17331751
17341752
/* Assume the connection has been established, reset the
17351753
Chat.timer.tidReconnect, and (if showMsg and
17361754
!!Chat.e.eMsgPollError) alert the user that the outage appears to
1737
- be over. Then schedule Chat.poll() to run in the very near
1755
+ be over. Also schedule Chat.poll() to run in the very near
17381756
future. */
1739
- const reportConnectionReestablished = function(dbgContext, showMsg = true){
1757
+ const reportConnectionOkay = function(dbgContext, showMsg = true){
17401758
if(Chat.beVerbose){
1741
- console.warn('reportConnectionReestablished', dbgContext,
1759
+ console.warn('reportConnectionOkay', dbgContext,
17421760
'Chat.e.pollErrorMarker =',Chat.e.pollErrorMarker,
17431761
'Chat.timer.tidReconnect =',Chat.timer.tidReconnect,
17441762
'Chat.timer =',Chat.timer);
17451763
}
1764
+ setTimeout( Chat.poll, Chat.timer.resetDelay() );
17461765
if( Chat.timer.errCount ){
17471766
D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
17481767
Chat.timer.errCount = 0;
17491768
}
17501769
if( Chat.timer.tidReconnect ){
@@ -1764,33 +1783,10 @@
17641783
}
17651784
m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
17661785
D.addClass(m.e.body,'poller-connection');
17671786
}
17681787
}
1769
- setTimeout( Chat.poll, Chat.timer.resetDelay() );
1770
- };
1771
-
1772
- /* To be called from F.fetch('chat-poll') beforesend() handler. If
1773
- we're currently in delayed-retry mode and a connection is
1774
- started, try to reset the delay after N time waiting on that
1775
- connection. The fact that the connection is waiting to respond,
1776
- rather than outright failing, is a good hint that the outage is
1777
- over and we can reset the back-off timer. */
1778
- const clearPollErrOnWait = function(){
1779
- //console.warn('clearPollErrOnWait outer', Chat.timer.tidReconnect, Chat.timer.currentDelay);
1780
- if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){
1781
- Chat.timer.tidReconnect = setTimeout(()=>{
1782
- //console.warn('clearPollErrOnWait inner');
1783
- Chat.timer.tidReconnect = 0;
1784
- if( poll.running ){
1785
- /* This chat-poll F.fetch() is still underway, so let's
1786
- assume the connection is back up until/unless it times
1787
- out or breaks again. */
1788
- reportConnectionReestablished('clearPollErrOnWait');
1789
- }
1790
- }, Chat.timer.$initialDelay * 3 );
1791
- }
17921788
};
17931789
17941790
/**
17951791
Submits the contents of the message input field (if not empty)
17961792
and/or the file attachment field to the server. If both are
@@ -1850,11 +1846,11 @@
18501846
onerror:function(err){
18511847
self.reportErrorAsMessage(err);
18521848
recoverFailedMessage(fallback);
18531849
},
18541850
onload:function(txt){
1855
- reportConnectionReestablished();
1851
+ reportConnectionOkay('chat-send');
18561852
if(!txt) return/*success response*/;
18571853
try{
18581854
const json = JSON.parse(txt);
18591855
self.newContent({msgs:[json]});
18601856
}catch(e){
@@ -2350,11 +2346,11 @@
23502346
/*filename needed for mimetype determination*/);
23512347
fd.append('render_mode',F.page.previewModes.wiki);
23522348
F.fetch('ajax/preview-text',{
23532349
payload: fd,
23542350
onload: function(html){
2355
- reportConnectionReestablished();
2351
+ reportConnectionOkay('ajax/preview-text');
23562352
Chat.setPreviewText(html);
23572353
F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
23582354
},
23592355
onerror: function(e){
23602356
F.fetch.onerror(e);
@@ -2488,11 +2484,11 @@
24882484
onerror:function(err){
24892485
Chat.reportErrorAsMessage(err);
24902486
Chat._isBatchLoading = false;
24912487
},
24922488
onload:function(x){
2493
- reportConnectionReestablished();
2489
+ reportConnectionOkay('loadOldMessages()');
24942490
let gotMessages = x.msgs.length;
24952491
newcontent(x,true);
24962492
Chat._isBatchLoading = false;
24972493
Chat.updateActiveUserList();
24982494
if(Chat._gotServerError){
@@ -2578,11 +2574,11 @@
25782574
onerror:function(err){
25792575
Chat.setCurrentView(Chat.e.viewMessages);
25802576
Chat.reportErrorAsMessage(err);
25812577
},
25822578
onload:function(jx){
2583
- reportConnectionReestablished();
2579
+ reportConnectionOkay('submitSearch()');
25842580
let previd = 0;
25852581
D.clearElement(eMsgTgt);
25862582
jx.msgs.forEach((m)=>{
25872583
m.isSearchResult = true;
25882584
const mw = new Chat.MessageWidget(m);
@@ -2611,10 +2607,39 @@
26112607
}
26122608
}
26132609
}
26142610
);
26152611
}/*Chat.submitSearch()*/;
2612
+
2613
+ /* To be called from F.fetch('chat-poll') beforesend() handler. If
2614
+ we're currently in delayed-retry mode and a connection is
2615
+ started, try to reset the delay after N time waiting on that
2616
+ connection. The fact that the connection is waiting to respond,
2617
+ rather than outright failing, is a good hint that the outage is
2618
+ over and we can reset the back-off timer.
2619
+
2620
+ Without this, recovery of a connection error won't be reported
2621
+ until after the long-poll completes by either receiving new
2622
+ messages or times out. Once a long-poll is in progress, though,
2623
+ we "know" that it's up and running again, so can update the UI
2624
+ and connection timer to reflect that.
2625
+ */
2626
+ const chatPollBeforeSend = function(){
2627
+ //console.warn('chatPollBeforeSend outer', Chat.timer.tidReconnect, Chat.timer.currentDelay);
2628
+ if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){
2629
+ Chat.timer.tidReconnect = setTimeout(()=>{
2630
+ //console.warn('chatPollBeforeSend inner');
2631
+ Chat.timer.tidReconnect = 0;
2632
+ if( poll.running ){
2633
+ /* This chat-poll F.fetch() is still underway, so let's
2634
+ assume the connection is back up until/unless it times
2635
+ out or breaks again. */
2636
+ reportConnectionOkay('chatPollBeforeSend', true);
2637
+ }
2638
+ }, Chat.timer.$initialDelay * 3 );
2639
+ }
2640
+ };
26162641
26172642
/**
26182643
Deal with the last poll() response and maybe re-start poll().
26192644
*/
26202645
const afterPollFetch = function f(err){
@@ -2640,23 +2665,27 @@
26402665
if( err && Chat.beVerbose ){
26412666
console.error("afterPollFetch:",err.name,err.status,err.message);
26422667
}
26432668
if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){
26442669
/* Restart the poller immediately. */
2645
- reportConnectionReestablished('afterPollFetch '+err, false);
2670
+ reportConnectionOkay('afterPollFetch '+err, false);
26462671
}else{
26472672
/* Delay a while before trying again, noting that other Chat
26482673
APIs may try and succeed at connections before this timer
26492674
resolves, in which case they'll clear this timeout and the
26502675
UI message about the outage. */
26512676
let delay;
26522677
D.addClass(Chat.e.pollErrorMarker, 'connection-error');
26532678
if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){
2679
+ delay = Chat.timer.resetDelay(
2680
+ (Chat.timer.minDelay * Chat.timer.errCount)
2681
+ + Chat.timer.randomInterval(Chat.timer.minDelay)
2682
+ );
26542683
if(Chat.beVerbose){
2655
- console.warn("Ignoring polling error #", Chat.timer.errCount);
2684
+ console.warn("Ignoring polling error #",Chat.timer.errCount,
2685
+ "for another",delay,"ms" );
26562686
}
2657
- delay = Chat.timer.resetDelay(Chat.timer.minDelay);
26582687
} else {
26592688
delay = Chat.timer.incrDelay();
26602689
//console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError);
26612690
const msg = "Poller connection error. Retrying in "+delay+ " ms.";
26622691
/* Replace the current/newest connection error widget. We could also
@@ -2700,77 +2729,71 @@
27002729
if(f.running) return;
27012730
f.running = true;
27022731
Chat._isBatchLoading = f.isFirstCall;
27032732
if(true===f.isFirstCall){
27042733
f.isFirstCall = false;
2705
- Chat.aPollErr = [];
2734
+ Chat.pendingOnError = undefined;
27062735
Chat.ajaxStart();
27072736
Chat.e.viewMessages.classList.add('loading');
2708
- setInterval(
2737
+ if(1) setInterval(
27092738
/*
27102739
We manager onerror() results in poll() using a
27112740
stack of error objects and we delay their handling by
27122741
a small amount, rather than immediately when the
27132742
exception arrives.
27142743
2715
- This level of indirection is to work around an inexplicable
2716
- behavior from the F.fetch() connections: timeouts are always
2717
- announced in pairs of an HTTP 0 and something we can
2718
- unambiguously identify as a timeout. When that happens, we
2719
- ignore the HTTP 0. If, however, an HTTP 0 is seen here
2720
- without an immediately-following timeout, we process
2721
- it. Attempts to squelch the HTTP 0 response at their source,
2722
- in F.fetch(), have led to worse breakage.
2744
+ This level of indirection is necessary to be able to
2745
+ unambiguously identify client-timeout-specific polling
2746
+ errors from other errors. Timeouts are always announced in
2747
+ pairs of an HTTP 0 and something we can unambiguously
2748
+ identify as a timeout. When we receive an HTTP 0 we put it
2749
+ into this queue. If an ontimeout() call arrives before this
2750
+ error is handled, this error is removed from the stack. If,
2751
+ however, an HTTP 0 is seen in this stack without an
2752
+ accompanying timeout, we handle it from here.
27232753
27242754
It's kinda like in the curses C API, where you to match
27252755
ALT-X by first getting an ESC event, then an X event, but
27262756
this one is a lot less explicable. (It's almost certainly a
27272757
mis-handling bug in F.fetch(), but it has so far eluded my
27282758
eyes.)
27292759
*/
27302760
()=>{
2731
- if( Chat.aPollErr.length ){
2732
- if(Chat.aPollErr.length>1){
2733
- //console.warn('aPollErr',Chat.aPollErr);
2734
- if(Chat.aPollErr[1].name==='timeout'){
2735
- /* mysterious pairs of HTTP 0 followed immediately
2736
- by timeout response; ignore the former in that case. */
2737
- Chat.aPollErr.shift();
2738
- }
2739
- }
2740
- afterPollFetch(Chat.aPollErr.shift());
2761
+ if( Chat.pendingOnError ){
2762
+ const x = Chat.pendingOnError;
2763
+ Chat.pendingOnError = undefined;
2764
+ afterPollFetch(x);
27412765
}
27422766
},
27432767
1000
27442768
);
27452769
}
27462770
let nErr = 0;
27472771
F.fetch("chat-poll",{
2748
- timeout: window.location.hostname.match(
2749
- "localhost" /*presumably local dev mode*/
2750
- ) ? 15000
2751
- : 420 * 1000/*FIXME: get the value from the server*/,
2772
+ timeout: Chat.timer.pollTimeout,
27522773
urlParams:{
27532774
name: Chat.mxMsg
27542775
},
27552776
responseType: "json",
27562777
// Disable the ajax start/end handling for this long-polling op:
2757
- beforesend: function(){
2758
- clearPollErrOnWait();
2759
- },
2778
+ beforesend: chatPollBeforeSend,
27602779
aftersend: function(){
27612780
poll.running = false;
2781
+ },
2782
+ ontimeout: function(err){
2783
+ Chat.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/;
2784
+ afterPollFetch(err);
27622785
},
27632786
onerror:function(err){
27642787
Chat._isBatchLoading = false;
27652788
if(Chat.beVerbose){
27662789
console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
27672790
}
2768
- Chat.aPollErr.push(err);
2791
+ Chat.pendingOnError = err;
27692792
},
27702793
onload:function(y){
2771
- reportConnectionReestablished('poll.onload', true);
2794
+ reportConnectionOkay('poll.onload', true);
27722795
newcontent(y);
27732796
if(Chat._isBatchLoading){
27742797
Chat._isBatchLoading = false;
27752798
Chat.updateActiveUserList();
27762799
}
27772800
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -191,28 +191,45 @@
191 when connection errors arrise. It starts off with a polling
192 delay of $initialDelay ms. If there's a connection error,
193 that gets bumped by some value for each subsequent error, up
194 to some max value.
195
196 The timeing 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 /* poller timer */,
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 /*timer id for reconnection determination*/,
 
 
208 errCount: 0 /* Current poller connection error count */,
209 minErrForNotify: 4 /* Don't warn for connection errors until this
210 many have occurred */,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211 randomInterval: function(factor){
212 return Math.floor(Math.random() * factor);
213 },
 
214 incrDelay: function(){
215 if( this.maxDelay > this.currentDelay ){
216 if(this.currentDelay < this.minDelay){
217 this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
218 }else{
@@ -219,13 +236,15 @@
219 this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
220 }
221 }
222 return this.currentDelay;
223 },
224 resetDelay: function(ms){
 
225 return this.currentDelay = ms || this.$initialDelay;
226 },
 
227 isDelayed: function(){
228 return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
229 }
230 },
231 /**
@@ -739,11 +758,10 @@
739 lmtime: d,
740 xmsg: args
741 });
742 this.injectMessageElem(mw.e.body);
743 mw.scrollIntoView();
744 //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction
745 return mw;
746 };
747
748 cs.getMessageElemById = function(id){
749 return qs('[data-msgid="'+id+'"]');
@@ -865,11 +883,11 @@
865 const self = this;
866 F.fetch('chat-fetch-one',{
867 urlParams:{ name: id, raw: true},
868 responseType: 'json',
869 onload: function(msg){
870 reportConnectionReestablished('chat-fetch-one');
871 content.$elems[1] = D.append(D.pre(),msg.xmsg);
872 content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
873 self.toggleTextMode(e);
874 },
875 aftersend:function(){
@@ -928,11 +946,11 @@
928 if(this.userMayDelete(e)){
929 this.ajaxStart();
930 F.fetch("chat-delete/" + id, {
931 responseType: 'json',
932 onload:(r)=>{
933 reportConnectionReestablished('chat-delete');
934 this.deleteMessageElem(r);
935 },
936 onerror:(err)=>this.reportErrorAsMessage(err)
937 });
938 }else{
@@ -1560,11 +1578,11 @@
1560 n: nFetch,
1561 i: iFirst
1562 },
1563 responseType: "json",
1564 onload:function(jx){
1565 reportConnectionReestablished('chat-query.onload');
1566 if( bDown ) jx.msgs.reverse();
1567 jx.msgs.forEach((m) => {
1568 m.isSearchResult = true;
1569 var mw = new Chat.MessageWidget(m);
1570 if( bDown ){
@@ -1732,19 +1750,20 @@
1732 };
1733
1734 /* Assume the connection has been established, reset the
1735 Chat.timer.tidReconnect, and (if showMsg and
1736 !!Chat.e.eMsgPollError) alert the user that the outage appears to
1737 be over. Then schedule Chat.poll() to run in the very near
1738 future. */
1739 const reportConnectionReestablished = function(dbgContext, showMsg = true){
1740 if(Chat.beVerbose){
1741 console.warn('reportConnectionReestablished', dbgContext,
1742 'Chat.e.pollErrorMarker =',Chat.e.pollErrorMarker,
1743 'Chat.timer.tidReconnect =',Chat.timer.tidReconnect,
1744 'Chat.timer =',Chat.timer);
1745 }
 
1746 if( Chat.timer.errCount ){
1747 D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
1748 Chat.timer.errCount = 0;
1749 }
1750 if( Chat.timer.tidReconnect ){
@@ -1764,33 +1783,10 @@
1764 }
1765 m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
1766 D.addClass(m.e.body,'poller-connection');
1767 }
1768 }
1769 setTimeout( Chat.poll, Chat.timer.resetDelay() );
1770 };
1771
1772 /* To be called from F.fetch('chat-poll') beforesend() handler. If
1773 we're currently in delayed-retry mode and a connection is
1774 started, try to reset the delay after N time waiting on that
1775 connection. The fact that the connection is waiting to respond,
1776 rather than outright failing, is a good hint that the outage is
1777 over and we can reset the back-off timer. */
1778 const clearPollErrOnWait = function(){
1779 //console.warn('clearPollErrOnWait outer', Chat.timer.tidReconnect, Chat.timer.currentDelay);
1780 if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){
1781 Chat.timer.tidReconnect = setTimeout(()=>{
1782 //console.warn('clearPollErrOnWait inner');
1783 Chat.timer.tidReconnect = 0;
1784 if( poll.running ){
1785 /* This chat-poll F.fetch() is still underway, so let's
1786 assume the connection is back up until/unless it times
1787 out or breaks again. */
1788 reportConnectionReestablished('clearPollErrOnWait');
1789 }
1790 }, Chat.timer.$initialDelay * 3 );
1791 }
1792 };
1793
1794 /**
1795 Submits the contents of the message input field (if not empty)
1796 and/or the file attachment field to the server. If both are
@@ -1850,11 +1846,11 @@
1850 onerror:function(err){
1851 self.reportErrorAsMessage(err);
1852 recoverFailedMessage(fallback);
1853 },
1854 onload:function(txt){
1855 reportConnectionReestablished();
1856 if(!txt) return/*success response*/;
1857 try{
1858 const json = JSON.parse(txt);
1859 self.newContent({msgs:[json]});
1860 }catch(e){
@@ -2350,11 +2346,11 @@
2350 /*filename needed for mimetype determination*/);
2351 fd.append('render_mode',F.page.previewModes.wiki);
2352 F.fetch('ajax/preview-text',{
2353 payload: fd,
2354 onload: function(html){
2355 reportConnectionReestablished();
2356 Chat.setPreviewText(html);
2357 F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
2358 },
2359 onerror: function(e){
2360 F.fetch.onerror(e);
@@ -2488,11 +2484,11 @@
2488 onerror:function(err){
2489 Chat.reportErrorAsMessage(err);
2490 Chat._isBatchLoading = false;
2491 },
2492 onload:function(x){
2493 reportConnectionReestablished();
2494 let gotMessages = x.msgs.length;
2495 newcontent(x,true);
2496 Chat._isBatchLoading = false;
2497 Chat.updateActiveUserList();
2498 if(Chat._gotServerError){
@@ -2578,11 +2574,11 @@
2578 onerror:function(err){
2579 Chat.setCurrentView(Chat.e.viewMessages);
2580 Chat.reportErrorAsMessage(err);
2581 },
2582 onload:function(jx){
2583 reportConnectionReestablished();
2584 let previd = 0;
2585 D.clearElement(eMsgTgt);
2586 jx.msgs.forEach((m)=>{
2587 m.isSearchResult = true;
2588 const mw = new Chat.MessageWidget(m);
@@ -2611,10 +2607,39 @@
2611 }
2612 }
2613 }
2614 );
2615 }/*Chat.submitSearch()*/;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2616
2617 /**
2618 Deal with the last poll() response and maybe re-start poll().
2619 */
2620 const afterPollFetch = function f(err){
@@ -2640,23 +2665,27 @@
2640 if( err && Chat.beVerbose ){
2641 console.error("afterPollFetch:",err.name,err.status,err.message);
2642 }
2643 if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){
2644 /* Restart the poller immediately. */
2645 reportConnectionReestablished('afterPollFetch '+err, false);
2646 }else{
2647 /* Delay a while before trying again, noting that other Chat
2648 APIs may try and succeed at connections before this timer
2649 resolves, in which case they'll clear this timeout and the
2650 UI message about the outage. */
2651 let delay;
2652 D.addClass(Chat.e.pollErrorMarker, 'connection-error');
2653 if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){
 
 
 
 
2654 if(Chat.beVerbose){
2655 console.warn("Ignoring polling error #", Chat.timer.errCount);
 
2656 }
2657 delay = Chat.timer.resetDelay(Chat.timer.minDelay);
2658 } else {
2659 delay = Chat.timer.incrDelay();
2660 //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError);
2661 const msg = "Poller connection error. Retrying in "+delay+ " ms.";
2662 /* Replace the current/newest connection error widget. We could also
@@ -2700,77 +2729,71 @@
2700 if(f.running) return;
2701 f.running = true;
2702 Chat._isBatchLoading = f.isFirstCall;
2703 if(true===f.isFirstCall){
2704 f.isFirstCall = false;
2705 Chat.aPollErr = [];
2706 Chat.ajaxStart();
2707 Chat.e.viewMessages.classList.add('loading');
2708 setInterval(
2709 /*
2710 We manager onerror() results in poll() using a
2711 stack of error objects and we delay their handling by
2712 a small amount, rather than immediately when the
2713 exception arrives.
2714
2715 This level of indirection is to work around an inexplicable
2716 behavior from the F.fetch() connections: timeouts are always
2717 announced in pairs of an HTTP 0 and something we can
2718 unambiguously identify as a timeout. When that happens, we
2719 ignore the HTTP 0. If, however, an HTTP 0 is seen here
2720 without an immediately-following timeout, we process
2721 it. Attempts to squelch the HTTP 0 response at their source,
2722 in F.fetch(), have led to worse breakage.
 
2723
2724 It's kinda like in the curses C API, where you to match
2725 ALT-X by first getting an ESC event, then an X event, but
2726 this one is a lot less explicable. (It's almost certainly a
2727 mis-handling bug in F.fetch(), but it has so far eluded my
2728 eyes.)
2729 */
2730 ()=>{
2731 if( Chat.aPollErr.length ){
2732 if(Chat.aPollErr.length>1){
2733 //console.warn('aPollErr',Chat.aPollErr);
2734 if(Chat.aPollErr[1].name==='timeout'){
2735 /* mysterious pairs of HTTP 0 followed immediately
2736 by timeout response; ignore the former in that case. */
2737 Chat.aPollErr.shift();
2738 }
2739 }
2740 afterPollFetch(Chat.aPollErr.shift());
2741 }
2742 },
2743 1000
2744 );
2745 }
2746 let nErr = 0;
2747 F.fetch("chat-poll",{
2748 timeout: window.location.hostname.match(
2749 "localhost" /*presumably local dev mode*/
2750 ) ? 15000
2751 : 420 * 1000/*FIXME: get the value from the server*/,
2752 urlParams:{
2753 name: Chat.mxMsg
2754 },
2755 responseType: "json",
2756 // Disable the ajax start/end handling for this long-polling op:
2757 beforesend: function(){
2758 clearPollErrOnWait();
2759 },
2760 aftersend: function(){
2761 poll.running = false;
 
 
 
 
2762 },
2763 onerror:function(err){
2764 Chat._isBatchLoading = false;
2765 if(Chat.beVerbose){
2766 console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
2767 }
2768 Chat.aPollErr.push(err);
2769 },
2770 onload:function(y){
2771 reportConnectionReestablished('poll.onload', true);
2772 newcontent(y);
2773 if(Chat._isBatchLoading){
2774 Chat._isBatchLoading = false;
2775 Chat.updateActiveUserList();
2776 }
2777
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -191,28 +191,45 @@
191 when connection errors arrise. It starts off with a polling
192 delay of $initialDelay ms. If there's a connection error,
193 that gets bumped by some value for each subsequent error, up
194 to some max value.
195
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*/
215 )) ? 15000
216 : (+F.config.chat.pollTimeout>0
217 ? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1)))
218 /* ^^^^^^^^^^^^ we want our timeouts to be slightly shorter
219 than the server's so that we can distingished timed-out
220 polls on our end from HTTP errors (if the server times
221 out). */
222 : 30000),
223 /** Returns a random fudge value for reconnect attempt times,
224 intended to keep the /chat server from getting hammered if
225 all clients which were just disconnected all reconnect at
226 the same instant. */
227 randomInterval: function(factor){
228 return Math.floor(Math.random() * factor);
229 },
230 /** Increments the reconnection delay, within some min/max range. */
231 incrDelay: function(){
232 if( this.maxDelay > this.currentDelay ){
233 if(this.currentDelay < this.minDelay){
234 this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
235 }else{
@@ -219,13 +236,15 @@
236 this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
237 }
238 }
239 return this.currentDelay;
240 },
241 /** Resets the delay counter to v || its initial value. */
242 resetDelay: function(ms=0){
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 /**
@@ -739,11 +758,10 @@
758 lmtime: d,
759 xmsg: args
760 });
761 this.injectMessageElem(mw.e.body);
762 mw.scrollIntoView();
 
763 return mw;
764 };
765
766 cs.getMessageElemById = function(id){
767 return qs('[data-msgid="'+id+'"]');
@@ -865,11 +883,11 @@
883 const self = this;
884 F.fetch('chat-fetch-one',{
885 urlParams:{ name: id, raw: true},
886 responseType: 'json',
887 onload: function(msg){
888 reportConnectionOkay('chat-fetch-one');
889 content.$elems[1] = D.append(D.pre(),msg.xmsg);
890 content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
891 self.toggleTextMode(e);
892 },
893 aftersend:function(){
@@ -928,11 +946,11 @@
946 if(this.userMayDelete(e)){
947 this.ajaxStart();
948 F.fetch("chat-delete/" + id, {
949 responseType: 'json',
950 onload:(r)=>{
951 reportConnectionOkay('chat-delete');
952 this.deleteMessageElem(r);
953 },
954 onerror:(err)=>this.reportErrorAsMessage(err)
955 });
956 }else{
@@ -1560,11 +1578,11 @@
1578 n: nFetch,
1579 i: iFirst
1580 },
1581 responseType: "json",
1582 onload:function(jx){
1583 reportConnectionOkay('chat-query');
1584 if( bDown ) jx.msgs.reverse();
1585 jx.msgs.forEach((m) => {
1586 m.isSearchResult = true;
1587 var mw = new Chat.MessageWidget(m);
1588 if( bDown ){
@@ -1732,19 +1750,20 @@
1750 };
1751
1752 /* Assume the connection has been established, reset the
1753 Chat.timer.tidReconnect, and (if showMsg and
1754 !!Chat.e.eMsgPollError) alert the user that the outage appears to
1755 be over. Also schedule Chat.poll() to run in the very near
1756 future. */
1757 const reportConnectionOkay = function(dbgContext, showMsg = true){
1758 if(Chat.beVerbose){
1759 console.warn('reportConnectionOkay', dbgContext,
1760 'Chat.e.pollErrorMarker =',Chat.e.pollErrorMarker,
1761 'Chat.timer.tidReconnect =',Chat.timer.tidReconnect,
1762 'Chat.timer =',Chat.timer);
1763 }
1764 setTimeout( Chat.poll, Chat.timer.resetDelay() );
1765 if( Chat.timer.errCount ){
1766 D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
1767 Chat.timer.errCount = 0;
1768 }
1769 if( Chat.timer.tidReconnect ){
@@ -1764,33 +1783,10 @@
1783 }
1784 m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
1785 D.addClass(m.e.body,'poller-connection');
1786 }
1787 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1788 };
1789
1790 /**
1791 Submits the contents of the message input field (if not empty)
1792 and/or the file attachment field to the server. If both are
@@ -1850,11 +1846,11 @@
1846 onerror:function(err){
1847 self.reportErrorAsMessage(err);
1848 recoverFailedMessage(fallback);
1849 },
1850 onload:function(txt){
1851 reportConnectionOkay('chat-send');
1852 if(!txt) return/*success response*/;
1853 try{
1854 const json = JSON.parse(txt);
1855 self.newContent({msgs:[json]});
1856 }catch(e){
@@ -2350,11 +2346,11 @@
2346 /*filename needed for mimetype determination*/);
2347 fd.append('render_mode',F.page.previewModes.wiki);
2348 F.fetch('ajax/preview-text',{
2349 payload: fd,
2350 onload: function(html){
2351 reportConnectionOkay('ajax/preview-text');
2352 Chat.setPreviewText(html);
2353 F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
2354 },
2355 onerror: function(e){
2356 F.fetch.onerror(e);
@@ -2488,11 +2484,11 @@
2484 onerror:function(err){
2485 Chat.reportErrorAsMessage(err);
2486 Chat._isBatchLoading = false;
2487 },
2488 onload:function(x){
2489 reportConnectionOkay('loadOldMessages()');
2490 let gotMessages = x.msgs.length;
2491 newcontent(x,true);
2492 Chat._isBatchLoading = false;
2493 Chat.updateActiveUserList();
2494 if(Chat._gotServerError){
@@ -2578,11 +2574,11 @@
2574 onerror:function(err){
2575 Chat.setCurrentView(Chat.e.viewMessages);
2576 Chat.reportErrorAsMessage(err);
2577 },
2578 onload:function(jx){
2579 reportConnectionOkay('submitSearch()');
2580 let previd = 0;
2581 D.clearElement(eMsgTgt);
2582 jx.msgs.forEach((m)=>{
2583 m.isSearchResult = true;
2584 const mw = new Chat.MessageWidget(m);
@@ -2611,10 +2607,39 @@
2607 }
2608 }
2609 }
2610 );
2611 }/*Chat.submitSearch()*/;
2612
2613 /* To be called from F.fetch('chat-poll') beforesend() handler. If
2614 we're currently in delayed-retry mode and a connection is
2615 started, try to reset the delay after N time waiting on that
2616 connection. The fact that the connection is waiting to respond,
2617 rather than outright failing, is a good hint that the outage is
2618 over and we can reset the back-off timer.
2619
2620 Without this, recovery of a connection error won't be reported
2621 until after the long-poll completes by either receiving new
2622 messages or times out. Once a long-poll is in progress, though,
2623 we "know" that it's up and running again, so can update the UI
2624 and connection timer to reflect that.
2625 */
2626 const chatPollBeforeSend = function(){
2627 //console.warn('chatPollBeforeSend outer', Chat.timer.tidReconnect, Chat.timer.currentDelay);
2628 if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){
2629 Chat.timer.tidReconnect = setTimeout(()=>{
2630 //console.warn('chatPollBeforeSend inner');
2631 Chat.timer.tidReconnect = 0;
2632 if( poll.running ){
2633 /* This chat-poll F.fetch() is still underway, so let's
2634 assume the connection is back up until/unless it times
2635 out or breaks again. */
2636 reportConnectionOkay('chatPollBeforeSend', true);
2637 }
2638 }, Chat.timer.$initialDelay * 3 );
2639 }
2640 };
2641
2642 /**
2643 Deal with the last poll() response and maybe re-start poll().
2644 */
2645 const afterPollFetch = function f(err){
@@ -2640,23 +2665,27 @@
2665 if( err && Chat.beVerbose ){
2666 console.error("afterPollFetch:",err.name,err.status,err.message);
2667 }
2668 if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){
2669 /* Restart the poller immediately. */
2670 reportConnectionOkay('afterPollFetch '+err, false);
2671 }else{
2672 /* Delay a while before trying again, noting that other Chat
2673 APIs may try and succeed at connections before this timer
2674 resolves, in which case they'll clear this timeout and the
2675 UI message about the outage. */
2676 let delay;
2677 D.addClass(Chat.e.pollErrorMarker, 'connection-error');
2678 if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){
2679 delay = Chat.timer.resetDelay(
2680 (Chat.timer.minDelay * Chat.timer.errCount)
2681 + Chat.timer.randomInterval(Chat.timer.minDelay)
2682 );
2683 if(Chat.beVerbose){
2684 console.warn("Ignoring polling error #",Chat.timer.errCount,
2685 "for another",delay,"ms" );
2686 }
 
2687 } else {
2688 delay = Chat.timer.incrDelay();
2689 //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError);
2690 const msg = "Poller connection error. Retrying in "+delay+ " ms.";
2691 /* Replace the current/newest connection error widget. We could also
@@ -2700,77 +2729,71 @@
2729 if(f.running) return;
2730 f.running = true;
2731 Chat._isBatchLoading = f.isFirstCall;
2732 if(true===f.isFirstCall){
2733 f.isFirstCall = false;
2734 Chat.pendingOnError = undefined;
2735 Chat.ajaxStart();
2736 Chat.e.viewMessages.classList.add('loading');
2737 if(1) setInterval(
2738 /*
2739 We manager onerror() results in poll() using a
2740 stack of error objects and we delay their handling by
2741 a small amount, rather than immediately when the
2742 exception arrives.
2743
2744 This level of indirection is necessary to be able to
2745 unambiguously identify client-timeout-specific polling
2746 errors from other errors. Timeouts are always announced in
2747 pairs of an HTTP 0 and something we can unambiguously
2748 identify as a timeout. When we receive an HTTP 0 we put it
2749 into this queue. If an ontimeout() call arrives before this
2750 error is handled, this error is removed from the stack. If,
2751 however, an HTTP 0 is seen in this stack without an
2752 accompanying timeout, we handle it from here.
2753
2754 It's kinda like in the curses C API, where you to match
2755 ALT-X by first getting an ESC event, then an X event, but
2756 this one is a lot less explicable. (It's almost certainly a
2757 mis-handling bug in F.fetch(), but it has so far eluded my
2758 eyes.)
2759 */
2760 ()=>{
2761 if( Chat.pendingOnError ){
2762 const x = Chat.pendingOnError;
2763 Chat.pendingOnError = undefined;
2764 afterPollFetch(x);
 
 
 
 
 
 
2765 }
2766 },
2767 1000
2768 );
2769 }
2770 let nErr = 0;
2771 F.fetch("chat-poll",{
2772 timeout: Chat.timer.pollTimeout,
 
 
 
2773 urlParams:{
2774 name: Chat.mxMsg
2775 },
2776 responseType: "json",
2777 // Disable the ajax start/end handling for this long-polling op:
2778 beforesend: chatPollBeforeSend,
 
 
2779 aftersend: function(){
2780 poll.running = false;
2781 },
2782 ontimeout: function(err){
2783 Chat.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/;
2784 afterPollFetch(err);
2785 },
2786 onerror:function(err){
2787 Chat._isBatchLoading = false;
2788 if(Chat.beVerbose){
2789 console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
2790 }
2791 Chat.pendingOnError = err;
2792 },
2793 onload:function(y){
2794 reportConnectionOkay('poll.onload', true);
2795 newcontent(y);
2796 if(Chat._isBatchLoading){
2797 Chat._isBatchLoading = false;
2798 Chat.updateActiveUserList();
2799 }
2800

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button