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: |
e8bbaf924f97764e3effd88f4d16dd79 |
| 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
Changes to src/chat.c.
| ︙ | ︙ | |||
252 253 254 255 256 257 258 |
@ 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)),
| | > | 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 | 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 | | | | > > > > > > > > > > > > | 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 |
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';
}
| | > | | > > > > > > | 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 |
/**
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.
| | | | > > > > > > > > > > > > > > > > > > | > | 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 |
msgid: "reconnect-"+(++InternalMsgId),
mtime: d,
lmtime: d,
xmsg: args
});
this.injectMessageElem(mw.e.body);
mw.scrollIntoView();
| < | 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 |
}
// 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){
| | | 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 |
}
if(!(e instanceof HTMLElement)) return;
if(this.userMayDelete(e)){
this.ajaxStart();
F.fetch("chat-delete/" + id, {
responseType: 'json',
onload:(r)=>{
| | | 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 |
urlParams:{
q: '',
n: nFetch,
i: iFirst
},
responseType: "json",
onload:function(jx){
| | | 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 |
}));
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
| | | | > | 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 |
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');
}
}
| < < < < < < < < < < < < < < < < < < < < < < < | 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 |
payload: fd,
responseType: 'text',
onerror:function(err){
self.reportErrorAsMessage(err);
recoverFailedMessage(fallback);
},
onload:function(txt){
| | | 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 |
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){
| | | 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 |
},
responseType: 'json',
onerror:function(err){
Chat.reportErrorAsMessage(err);
Chat._isBatchLoading = false;
},
onload:function(x){
| | | 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 |
payload: fd,
responseType: 'json',
onerror:function(err){
Chat.setCurrentView(Chat.e.viewMessages);
Chat.reportErrorAsMessage(err);
},
onload:function(jx){
| | | 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 |
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. */
| | > > > > | > < | 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 |
*/
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;
| | | | > | | | > > | | < < | | < < < < | < < | | < < < | < < > > > > | | | 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();
}
|
| ︙ | ︙ |