Fossil SCM

If /chat's poller cannot connect to the server, apply a back-off timer so that it does not keep hammering the remote every single second. It attempts to inform the user about outages and when reconnection has succeeded, but it's difficult to test the timing of the the UI elements thoroughly with a single pair of hands, so this is being checked in for dogfooding.

stephan 2025-04-09 22:37 trunk
Commit 2debc54e67ec71fa66f0a6b025713e97734cbfd5c65eed23dd77d54af1ef2a33
--- 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,45 @@
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
+ id: undefined,
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: 7000 /* minimum delay time */,
203
+ idReconnect: undefined /*timer id for reconnection determination*/,
204
+ incrDelay: function(){
205
+ if( this.maxDelay > this.currentDelay ){
206
+ this.currentDelay =
207
+ (this.currentDelay < this.minDelay)
208
+ ? this.minDelay
209
+ : (this.currentDelay * 3);
210
+ }
211
+ return this.currentDelay;
212
+ },
213
+ resetDelay: function(){
214
+ return this.currentDelay = this.$initialDelay;
215
+ },
216
+ isDelayed: function(){
217
+ return this.currentDelay > this.$initialDelay;
218
+ }
219
+ },
185220
/**
186221
Gets (no args) or sets (1 arg) the current input text field
187222
value, taking into account single- vs multi-line input. The
188223
getter returns a trim()'d string and the setter returns this
189224
object. As a special case, if arguments[0] is a boolean
@@ -606,11 +641,11 @@
606641
607642
/**
608643
If animations are enabled, passes its arguments
609644
to D.addClassBriefly(), else this is a no-op.
610645
If cb is a function, it is called after the
611
- CSS class is removed. Returns this object;
646
+ CSS class is removed. Returns this object;
612647
*/
613648
animate: function f(e,a,cb){
614649
if(!f.$disabled){
615650
D.addClassBriefly(e, a, 0, cb);
616651
}
@@ -645,33 +680,59 @@
645680
cs.reportError = function(/*msg args*/){
646681
const args = argsToArray(arguments);
647682
console.error("chat error:",args);
648683
F.toast.error.apply(F.toast, args);
649684
};
685
+
686
+ let InternalMsgId = 0;
650687
/**
651688
Reports an error in the form of a new message in the chat
652689
feed. All arguments are appended to the message's content area
653690
using fossil.dom.append(), so may be of any type supported by
654691
that function.
655692
*/
656693
cs.reportErrorAsMessage = function f(/*msg args*/){
657
- if(undefined === f.$msgid) f.$msgid=0;
658694
const args = argsToArray(arguments).map(function(v){
659695
return (v instanceof Error) ? v.message : v;
660696
});
661
- console.error("chat error:",args);
697
+ if(Chat.verboseErrors){
698
+ console.error("chat error:",args);
699
+ }
662700
const d = new Date().toISOString(),
663701
mw = new this.MessageWidget({
664702
isError: true,
665
- xfrom: null,
666
- msgid: "error-"+(++f.$msgid),
703
+ xfrom: undefined,
704
+ msgid: "error-"+(++InternalMsgId),
705
+ mtime: d,
706
+ lmtime: d,
707
+ xmsg: args
708
+ });
709
+ this.injectMessageElem(mw.e.body);
710
+ mw.scrollIntoView();
711
+ return mw;
712
+ };
713
+
714
+ /**
715
+ For use by the connection poller to send a "connection
716
+ restored" message.
717
+ */
718
+ cs.reportReconnection = function f(/*msg args*/){
719
+ const args = argsToArray(arguments).map(function(v){
720
+ return (v instanceof Error) ? v.message : v;
721
+ });
722
+ const d = new Date().toISOString(),
723
+ mw = new this.MessageWidget({
724
+ isError: false,
725
+ xfrom: undefined,
726
+ msgid: "reconnect-"+(++InternalMsgId),
667727
mtime: d,
668728
lmtime: d,
669729
xmsg: args
670730
});
671731
this.injectMessageElem(mw.e.body);
672732
mw.scrollIntoView();
733
+ return mw;
673734
};
674735
675736
cs.getMessageElemById = function(id){
676737
return qs('[data-msgid="'+id+'"]');
677738
};
@@ -1624,10 +1685,45 @@
16241685
const theMsg = findMessageWidgetParent(w);
16251686
if(theMsg) Chat.deleteMessageElem(theMsg);
16261687
}));
16271688
Chat.reportErrorAsMessage(w);
16281689
};
1690
+
1691
+ /* Assume the connection has been established, reset
1692
+ the Chat.timer.idReconnect, and alert the user
1693
+ that the outage appears to be over. */
1694
+ const reportConnectionReestablished = function(){
1695
+ if( Chat.timer.idReconnect ){
1696
+ clearTimeout(Chat.timer.idReconnect);
1697
+ Chat.timer.idReconnect = 0;
1698
+ }
1699
+ if( Chat.timer.isDelayed() ){
1700
+ Chat.timer.resetDelay();
1701
+ Chat.reportReconnection(
1702
+ "Connection restored after outage."
1703
+ );
1704
+ setTimeout( Chat.poll, 0 );
1705
+ }
1706
+ };
1707
+
1708
+ /* If we're currently in delayed-retry mode, try to reset the delay
1709
+ if we're waiting for a while for the connection to complete,
1710
+ as that's an indication (not a guaranty) that we're connected
1711
+ and long-polling. */
1712
+ const setupConnectionReestablished = function(){
1713
+ if( !Chat.timer.idReconnect && Chat.timer.isDelayed() ){
1714
+ Chat.timer.idReconnect = setTimeout(()=>{
1715
+ Chat.timer.idReconnect = 0;
1716
+ if( poll.running ){
1717
+ reportConnectionReestablished();
1718
+ }
1719
+ }, Chat.timer.$initialDelay * 5 );
1720
+ Chat.e.viewMessages.querySelectorAll(
1721
+ '.message-widget.error-connection'
1722
+ ).forEach(e=>D.remove(e));
1723
+ }
1724
+ };
16291725
16301726
/**
16311727
Submits the contents of the message input field (if not empty)
16321728
and/or the file attachment field to the server. If both are
16331729
empty, this is a no-op.
@@ -1681,15 +1777,20 @@
16811777
const self = this;
16821778
fd.set("lmtime", localTime8601(new Date()));
16831779
F.fetch("chat-send",{
16841780
payload: fd,
16851781
responseType: 'text',
1782
+ beforesend: function(){
1783
+ Chat.ajaxStart();
1784
+ setupConnectionReestablished();
1785
+ },
16861786
onerror:function(err){
16871787
self.reportErrorAsMessage(err);
16881788
recoverFailedMessage(fallback);
16891789
},
16901790
onload:function(txt){
1791
+ reportConnectionReestablished();
16911792
if(!txt) return/*success response*/;
16921793
try{
16931794
const json = JSON.parse(txt);
16941795
self.newContent({msgs:[json]});
16951796
}catch(e){
@@ -2444,29 +2545,56 @@
24442545
}
24452546
}
24462547
);
24472548
}/*Chat.submitSearch()*/;
24482549
2449
- const afterFetch = function f(){
2550
+ /**
2551
+ Deal with the last poll() response and maybe re-start poll().
2552
+ */
2553
+ const afterPollFetch = function f(isOkay = true){
24502554
if(true===f.isFirstCall){
24512555
f.isFirstCall = false;
24522556
Chat.ajaxEnd();
24532557
Chat.e.viewMessages.classList.remove('loading');
24542558
setTimeout(function(){
24552559
Chat.scrollMessagesTo(1);
24562560
}, 250);
24572561
}
2458
- if(Chat._gotServerError && Chat.intervalTimer){
2459
- clearInterval(Chat.intervalTimer);
2562
+ if(Chat.timer.id) {
2563
+ clearTimeout(Chat.timer.id);
2564
+ Chat.timer.id = 0;
2565
+ }
2566
+ if(Chat._gotServerError){
24602567
Chat.reportErrorAsMessage(
24612568
"Shutting down chat poller due to server-side error. ",
24622569
"Reload this page to reactivate it.");
2463
- delete Chat.intervalTimer;
2570
+ Chat.timer.id = undefined;
2571
+ poll.running = false;
2572
+ } else {
2573
+ poll.running = false;
2574
+ if( isOkay ){
2575
+ Chat.timer.id = setTimeout(
2576
+ poll, Chat.timer.resetDelay()
2577
+ );
2578
+ }else{
2579
+ const delay = Chat.timer.incrDelay();
2580
+ const msg = D.addClass(
2581
+ Chat.reportErrorAsMessage(
2582
+ "Connection error. Retrying in ",
2583
+ delay, " ms.").e.body,
2584
+ 'error-connection'
2585
+ );
2586
+ Chat.timer.id = setTimeout(()=>{
2587
+ D.remove(msg);
2588
+ poll();
2589
+ }, delay);
2590
+ }
2591
+ //console.log("isOkay =",isOkay,"currentDelay =",Chat.timer.currentDelay);
24642592
}
2465
- poll.running = false;
24662593
};
2467
- afterFetch.isFirstCall = true;
2594
+ afterPollFetch.isFirstCall = true;
2595
+
24682596
/**
24692597
FIXME: when polling fails because the remote server is
24702598
reachable but it's not accepting HTTP requests, we should back
24712599
off on polling for a while. e.g. if the remote web server process
24722600
is killed, the poll fails quickly and immediately retries,
@@ -2489,42 +2617,48 @@
24892617
Chat.ajaxStart();
24902618
Chat.e.viewMessages.classList.add('loading');
24912619
}
24922620
F.fetch("chat-poll",{
24932621
timeout: 420 * 1000/*FIXME: get the value from the server*/,
2622
+ //timeout: 8000,
24942623
urlParams:{
24952624
name: Chat.mxMsg
24962625
},
24972626
responseType: "json",
24982627
// Disable the ajax start/end handling for this long-polling op:
2499
- beforesend: function(){},
2500
- aftersend: function(){},
2628
+ beforesend: function(){
2629
+ setupConnectionReestablished();
2630
+ },
2631
+ aftersend: function(){
2632
+ },
25012633
onerror:function(err){
25022634
Chat._isBatchLoading = false;
2503
- if(Chat.verboseErrors) console.error(err);
2635
+ if(Chat.verboseErrors) console.error("poll onerror:",err);
25042636
/* ^^^ we don't use Chat.reportError() here b/c the polling
25052637
fails exepectedly when it times out, but is then immediately
25062638
resumed, and reportError() produces a loud error message. */
2507
- afterFetch();
2639
+ afterPollFetch(false);
25082640
},
25092641
onload:function(y){
2642
+ reportConnectionReestablished();
25102643
newcontent(y);
25112644
if(Chat._isBatchLoading){
25122645
Chat._isBatchLoading = false;
25132646
Chat.updateActiveUserList();
25142647
}
2515
- afterFetch();
2648
+ afterPollFetch(true);
25162649
}
25172650
});
25182651
};
25192652
poll.isFirstCall = true;
2653
+ Chat.poll = poll;
25202654
Chat._gotServerError = poll.running = false;
25212655
if( window.fossil.config.chat.fromcli ){
25222656
Chat.chatOnlyMode(true);
25232657
}
2524
- Chat.intervalTimer = setInterval(poll, 1000);
2658
+ Chat.timer.id = setTimeout(poll, Chat.timer.resetDelay());
25252659
delete ForceResizeKludge.$disabled;
25262660
ForceResizeKludge();
25272661
Chat.animate.$disabled = false;
25282662
setTimeout( ()=>Chat.inputFocus(), 0 );
25292663
F.page.chat = Chat/* enables testing the APIs via the dev tools */;
25302664
});
25312665
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -180,10 +180,45 @@
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
@@ -606,11 +641,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 +680,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 };
@@ -1624,10 +1685,45 @@
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.
@@ -1681,15 +1777,20 @@
1681 const self = this;
1682 fd.set("lmtime", localTime8601(new Date()));
1683 F.fetch("chat-send",{
1684 payload: fd,
1685 responseType: 'text',
 
 
 
 
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){
@@ -2444,29 +2545,56 @@
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 +2617,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,45 @@
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 id: undefined,
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: 7000 /* minimum delay time */,
203 idReconnect: undefined /*timer id for reconnection determination*/,
204 incrDelay: function(){
205 if( this.maxDelay > this.currentDelay ){
206 this.currentDelay =
207 (this.currentDelay < this.minDelay)
208 ? this.minDelay
209 : (this.currentDelay * 3);
210 }
211 return this.currentDelay;
212 },
213 resetDelay: function(){
214 return this.currentDelay = this.$initialDelay;
215 },
216 isDelayed: function(){
217 return this.currentDelay > this.$initialDelay;
218 }
219 },
220 /**
221 Gets (no args) or sets (1 arg) the current input text field
222 value, taking into account single- vs multi-line input. The
223 getter returns a trim()'d string and the setter returns this
224 object. As a special case, if arguments[0] is a boolean
@@ -606,11 +641,11 @@
641
642 /**
643 If animations are enabled, passes its arguments
644 to D.addClassBriefly(), else this is a no-op.
645 If cb is a function, it is called after the
646 CSS class is removed. Returns this object;
647 */
648 animate: function f(e,a,cb){
649 if(!f.$disabled){
650 D.addClassBriefly(e, a, 0, cb);
651 }
@@ -645,33 +680,59 @@
680 cs.reportError = function(/*msg args*/){
681 const args = argsToArray(arguments);
682 console.error("chat error:",args);
683 F.toast.error.apply(F.toast, args);
684 };
685
686 let InternalMsgId = 0;
687 /**
688 Reports an error in the form of a new message in the chat
689 feed. All arguments are appended to the message's content area
690 using fossil.dom.append(), so may be of any type supported by
691 that function.
692 */
693 cs.reportErrorAsMessage = function f(/*msg args*/){
 
694 const args = argsToArray(arguments).map(function(v){
695 return (v instanceof Error) ? v.message : v;
696 });
697 if(Chat.verboseErrors){
698 console.error("chat error:",args);
699 }
700 const d = new Date().toISOString(),
701 mw = new this.MessageWidget({
702 isError: true,
703 xfrom: undefined,
704 msgid: "error-"+(++InternalMsgId),
705 mtime: d,
706 lmtime: d,
707 xmsg: args
708 });
709 this.injectMessageElem(mw.e.body);
710 mw.scrollIntoView();
711 return mw;
712 };
713
714 /**
715 For use by the connection poller to send a "connection
716 restored" message.
717 */
718 cs.reportReconnection = function f(/*msg args*/){
719 const args = argsToArray(arguments).map(function(v){
720 return (v instanceof Error) ? v.message : v;
721 });
722 const d = new Date().toISOString(),
723 mw = new this.MessageWidget({
724 isError: false,
725 xfrom: undefined,
726 msgid: "reconnect-"+(++InternalMsgId),
727 mtime: d,
728 lmtime: d,
729 xmsg: args
730 });
731 this.injectMessageElem(mw.e.body);
732 mw.scrollIntoView();
733 return mw;
734 };
735
736 cs.getMessageElemById = function(id){
737 return qs('[data-msgid="'+id+'"]');
738 };
@@ -1624,10 +1685,45 @@
1685 const theMsg = findMessageWidgetParent(w);
1686 if(theMsg) Chat.deleteMessageElem(theMsg);
1687 }));
1688 Chat.reportErrorAsMessage(w);
1689 };
1690
1691 /* Assume the connection has been established, reset
1692 the Chat.timer.idReconnect, and alert the user
1693 that the outage appears to be over. */
1694 const reportConnectionReestablished = function(){
1695 if( Chat.timer.idReconnect ){
1696 clearTimeout(Chat.timer.idReconnect);
1697 Chat.timer.idReconnect = 0;
1698 }
1699 if( Chat.timer.isDelayed() ){
1700 Chat.timer.resetDelay();
1701 Chat.reportReconnection(
1702 "Connection restored after outage."
1703 );
1704 setTimeout( Chat.poll, 0 );
1705 }
1706 };
1707
1708 /* If we're currently in delayed-retry mode, try to reset the delay
1709 if we're waiting for a while for the connection to complete,
1710 as that's an indication (not a guaranty) that we're connected
1711 and long-polling. */
1712 const setupConnectionReestablished = function(){
1713 if( !Chat.timer.idReconnect && Chat.timer.isDelayed() ){
1714 Chat.timer.idReconnect = setTimeout(()=>{
1715 Chat.timer.idReconnect = 0;
1716 if( poll.running ){
1717 reportConnectionReestablished();
1718 }
1719 }, Chat.timer.$initialDelay * 5 );
1720 Chat.e.viewMessages.querySelectorAll(
1721 '.message-widget.error-connection'
1722 ).forEach(e=>D.remove(e));
1723 }
1724 };
1725
1726 /**
1727 Submits the contents of the message input field (if not empty)
1728 and/or the file attachment field to the server. If both are
1729 empty, this is a no-op.
@@ -1681,15 +1777,20 @@
1777 const self = this;
1778 fd.set("lmtime", localTime8601(new Date()));
1779 F.fetch("chat-send",{
1780 payload: fd,
1781 responseType: 'text',
1782 beforesend: function(){
1783 Chat.ajaxStart();
1784 setupConnectionReestablished();
1785 },
1786 onerror:function(err){
1787 self.reportErrorAsMessage(err);
1788 recoverFailedMessage(fallback);
1789 },
1790 onload:function(txt){
1791 reportConnectionReestablished();
1792 if(!txt) return/*success response*/;
1793 try{
1794 const json = JSON.parse(txt);
1795 self.newContent({msgs:[json]});
1796 }catch(e){
@@ -2444,29 +2545,56 @@
2545 }
2546 }
2547 );
2548 }/*Chat.submitSearch()*/;
2549
2550 /**
2551 Deal with the last poll() response and maybe re-start poll().
2552 */
2553 const afterPollFetch = function f(isOkay = true){
2554 if(true===f.isFirstCall){
2555 f.isFirstCall = false;
2556 Chat.ajaxEnd();
2557 Chat.e.viewMessages.classList.remove('loading');
2558 setTimeout(function(){
2559 Chat.scrollMessagesTo(1);
2560 }, 250);
2561 }
2562 if(Chat.timer.id) {
2563 clearTimeout(Chat.timer.id);
2564 Chat.timer.id = 0;
2565 }
2566 if(Chat._gotServerError){
2567 Chat.reportErrorAsMessage(
2568 "Shutting down chat poller due to server-side error. ",
2569 "Reload this page to reactivate it.");
2570 Chat.timer.id = undefined;
2571 poll.running = false;
2572 } else {
2573 poll.running = false;
2574 if( isOkay ){
2575 Chat.timer.id = setTimeout(
2576 poll, Chat.timer.resetDelay()
2577 );
2578 }else{
2579 const delay = Chat.timer.incrDelay();
2580 const msg = D.addClass(
2581 Chat.reportErrorAsMessage(
2582 "Connection error. Retrying in ",
2583 delay, " ms.").e.body,
2584 'error-connection'
2585 );
2586 Chat.timer.id = setTimeout(()=>{
2587 D.remove(msg);
2588 poll();
2589 }, delay);
2590 }
2591 //console.log("isOkay =",isOkay,"currentDelay =",Chat.timer.currentDelay);
2592 }
 
2593 };
2594 afterPollFetch.isFirstCall = true;
2595
2596 /**
2597 FIXME: when polling fails because the remote server is
2598 reachable but it's not accepting HTTP requests, we should back
2599 off on polling for a while. e.g. if the remote web server process
2600 is killed, the poll fails quickly and immediately retries,
@@ -2489,42 +2617,48 @@
2617 Chat.ajaxStart();
2618 Chat.e.viewMessages.classList.add('loading');
2619 }
2620 F.fetch("chat-poll",{
2621 timeout: 420 * 1000/*FIXME: get the value from the server*/,
2622 //timeout: 8000,
2623 urlParams:{
2624 name: Chat.mxMsg
2625 },
2626 responseType: "json",
2627 // Disable the ajax start/end handling for this long-polling op:
2628 beforesend: function(){
2629 setupConnectionReestablished();
2630 },
2631 aftersend: function(){
2632 },
2633 onerror:function(err){
2634 Chat._isBatchLoading = false;
2635 if(Chat.verboseErrors) console.error("poll onerror:",err);
2636 /* ^^^ we don't use Chat.reportError() here b/c the polling
2637 fails exepectedly when it times out, but is then immediately
2638 resumed, and reportError() produces a loud error message. */
2639 afterPollFetch(false);
2640 },
2641 onload:function(y){
2642 reportConnectionReestablished();
2643 newcontent(y);
2644 if(Chat._isBatchLoading){
2645 Chat._isBatchLoading = false;
2646 Chat.updateActiveUserList();
2647 }
2648 afterPollFetch(true);
2649 }
2650 });
2651 };
2652 poll.isFirstCall = true;
2653 Chat.poll = poll;
2654 Chat._gotServerError = poll.running = false;
2655 if( window.fossil.config.chat.fromcli ){
2656 Chat.chatOnlyMode(true);
2657 }
2658 Chat.timer.id = setTimeout(poll, Chat.timer.resetDelay());
2659 delete ForceResizeKludge.$disabled;
2660 ForceResizeKludge();
2661 Chat.animate.$disabled = false;
2662 setTimeout( ()=>Chat.inputFocus(), 0 );
2663 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
2664 });
2665

Keyboard Shortcuts

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