Fossil SCM

Add a connection retry back-off delay to /chat when it cannot reach the server.

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

Keyboard Shortcuts

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