2018-03-25 11:09:17 -04:00
import utils from './utils.js' ;
2019-10-20 10:00:18 +02:00
import toastService from "./toast.js" ;
2019-12-09 23:07:45 +01:00
import server from "./server.js" ;
2020-02-05 22:46:20 +01:00
import options from "./options.js" ;
2021-08-20 21:42:06 +02:00
import frocaUpdater from "./froca_updater.js" ;
2022-12-01 13:07:23 +01:00
import appContext from "../components/app_context.js" ;
2017-11-28 17:52:47 -05:00
2018-03-25 21:16:57 -04:00
const messageHandlers = [ ] ;
2018-03-25 13:08:58 -04:00
let ws ;
2020-08-02 23:27:48 +02:00
let lastAcceptedEntityChangeId = window . glob . maxEntityChangeIdAtLoad ;
2021-03-21 22:43:41 +01:00
let lastAcceptedEntityChangeSyncId = window . glob . maxEntityChangeSyncIdAtLoad ;
2020-08-02 23:27:48 +02:00
let lastProcessedEntityChangeId = window . glob . maxEntityChangeIdAtLoad ;
2018-03-25 13:08:58 -04:00
let lastPingTs ;
2021-03-21 22:43:41 +01:00
let frontendUpdateDataQueue = [ ] ;
2018-03-25 13:08:58 -04:00
2018-03-25 11:09:17 -04:00
function logError ( message ) {
2020-10-12 21:05:34 +02:00
console . error ( utils . now ( ) , message ) ; // needs to be separate from .trace()
2017-12-19 23:22:21 -05:00
2018-03-25 11:09:17 -04:00
if ( ws && ws . readyState === 1 ) {
ws . send ( JSON . stringify ( {
type : 'log-error' ,
2020-06-10 00:10:27 +02:00
error : message ,
stack : new Error ( ) . stack
2018-03-25 11:09:17 -04:00
} ) ) ;
}
}
2017-12-17 13:46:18 -05:00
2021-09-17 22:34:23 +02:00
function logInfo ( message ) {
console . log ( utils . now ( ) , message ) ;
if ( ws && ws . readyState === 1 ) {
ws . send ( JSON . stringify ( {
type : 'log-info' ,
info : message
} ) ) ;
}
}
2020-10-12 21:05:34 +02:00
window . logError = logError ;
2021-09-17 22:34:23 +02:00
window . logInfo = logInfo ;
2020-10-12 21:05:34 +02:00
2018-03-25 21:16:57 -04:00
function subscribeToMessages ( messageHandler ) {
messageHandlers . push ( messageHandler ) ;
}
2021-03-21 22:43:41 +01:00
// used to serialize frontend update operations
2019-10-20 17:49:58 +02:00
let consumeQueuePromise = null ;
2020-08-02 23:27:48 +02:00
// to make sure each change event is processed only once. Not clear if this is still necessary
const processedEntityChangeIds = new Set ( ) ;
2020-05-14 13:08:06 +02:00
2020-12-14 14:17:51 +01:00
function logRows ( entityChanges ) {
const filteredRows = entityChanges . filter ( row =>
2020-08-02 23:27:48 +02:00
! processedEntityChangeIds . has ( row . id )
2020-05-14 13:08:06 +02:00
&& ( row . entityName !== 'options' || row . entityId !== 'openTabs' ) ) ;
if ( filteredRows . length > 0 ) {
2021-03-21 22:43:41 +01:00
console . debug ( utils . now ( ) , "Frontend update data: " , filteredRows ) ;
2020-05-14 13:08:06 +02:00
}
}
2020-05-12 13:40:42 +02:00
2021-09-12 11:18:06 +02:00
async function executeFrontendUpdate ( entityChanges ) {
lastPingTs = Date . now ( ) ;
2017-12-19 23:22:21 -05:00
2021-09-12 11:18:06 +02:00
if ( entityChanges . length > 0 ) {
logRows ( entityChanges ) ;
2018-08-01 09:26:02 +02:00
2021-09-12 11:18:06 +02:00
frontendUpdateDataQueue . push ( ... entityChanges ) ;
2018-01-06 22:56:54 -05:00
2021-09-12 11:18:06 +02:00
// we set lastAcceptedEntityChangeId even before frontend update processing and send ping so that backend can start sending more updates
2020-05-12 13:40:42 +02:00
2021-10-26 22:07:35 +02:00
for ( const entityChange of entityChanges ) {
lastAcceptedEntityChangeId = Math . max ( lastAcceptedEntityChangeId , entityChange . id ) ;
2017-11-28 17:52:47 -05:00
2021-10-26 22:07:35 +02:00
if ( entityChange . isSynced ) {
lastAcceptedEntityChangeSyncId = Math . max ( lastAcceptedEntityChangeSyncId , entityChange . id ) ;
}
2021-09-12 11:18:06 +02:00
}
2021-03-21 22:43:41 +01:00
2021-09-12 11:18:06 +02:00
sendPing ( ) ;
2021-03-21 22:43:41 +01:00
2021-09-12 11:18:06 +02:00
// first wait for all the preceding consumers to finish
while ( consumeQueuePromise ) {
await consumeQueuePromise ;
}
2021-03-21 22:43:41 +01:00
2021-09-12 11:18:06 +02:00
try {
// it's my turn so start it up
consumeQueuePromise = consumeFrontendUpdateData ( ) ;
2019-12-16 22:47:07 +01:00
2021-09-12 11:18:06 +02:00
await consumeQueuePromise ;
} finally {
// finish and set to null to signal somebody else can pick it up
consumeQueuePromise = null ;
}
}
}
2019-08-06 22:39:27 +02:00
2021-09-12 11:18:06 +02:00
async function handleMessage ( event ) {
const message = JSON . parse ( event . data ) ;
2017-11-28 17:52:47 -05:00
2021-09-12 11:18:06 +02:00
for ( const messageHandler of messageHandlers ) {
messageHandler ( message ) ;
}
2021-09-26 15:37:18 +02:00
if ( message . type === 'ping' ) {
lastPingTs = Date . now ( ) ;
}
else if ( message . type === 'reload-frontend' ) {
2021-09-17 22:34:23 +02:00
utils . reloadFrontendApp ( "received request from backend to reload frontend" ) ;
2021-09-12 11:18:06 +02:00
}
else if ( message . type === 'frontend-update' ) {
await executeFrontendUpdate ( message . data . entityChanges ) ;
2018-03-25 11:09:17 -04:00
}
else if ( message . type === 'sync-hash-check-failed' ) {
2019-10-20 10:00:18 +02:00
toastService . showError ( "Sync check failed!" , 60000 ) ;
2017-11-28 17:52:47 -05:00
}
2018-03-25 11:09:17 -04:00
else if ( message . type === 'consistency-checks-failed' ) {
2019-10-20 10:00:18 +02:00
toastService . showError ( "Consistency checks failed! See logs for details." , 50 * 60000 ) ;
2018-03-25 11:09:17 -04:00
}
2022-09-17 23:06:17 +02:00
else if ( message . type === 'api-log-messages' ) {
appContext . triggerEvent ( "apiLogMessages" , { noteId : message . noteId , messages : message . messages } ) ;
}
2018-03-25 11:09:17 -04:00
}
2020-08-02 23:27:48 +02:00
let entityChangeIdReachedListeners = [ ] ;
2019-10-20 17:49:58 +02:00
2020-08-02 23:27:48 +02:00
function waitForEntityChangeId ( desiredEntityChangeId ) {
if ( desiredEntityChangeId <= lastProcessedEntityChangeId ) {
2019-10-20 17:49:58 +02:00
return Promise . resolve ( ) ;
}
2021-02-20 23:17:29 +01:00
console . debug ( ` Waiting for ${ desiredEntityChangeId } , last processed is ${ lastProcessedEntityChangeId } , last accepted ${ lastAcceptedEntityChangeId } ` ) ;
2020-03-10 21:33:03 +01:00
2019-10-20 17:49:58 +02:00
return new Promise ( ( res , rej ) => {
2020-08-02 23:27:48 +02:00
entityChangeIdReachedListeners . push ( {
desiredEntityChangeId : desiredEntityChangeId ,
2019-10-24 23:02:29 +02:00
resolvePromise : res ,
start : Date . now ( )
2019-10-20 17:49:58 +02:00
} )
} ) ;
}
2020-08-02 23:27:48 +02:00
function waitForMaxKnownEntityChangeId ( ) {
return waitForEntityChangeId ( server . getMaxKnownEntityChangeId ( ) ) ;
2019-12-09 23:07:45 +01:00
}
2020-08-02 23:27:48 +02:00
function checkEntityChangeIdListeners ( ) {
entityChangeIdReachedListeners
. filter ( l => l . desiredEntityChangeId <= lastProcessedEntityChangeId )
2019-10-28 19:45:36 +01:00
. forEach ( l => l . resolvePromise ( ) ) ;
2020-08-02 23:27:48 +02:00
entityChangeIdReachedListeners = entityChangeIdReachedListeners
. filter ( l => l . desiredEntityChangeId > lastProcessedEntityChangeId ) ;
2019-10-28 19:45:36 +01:00
2020-08-02 23:27:48 +02:00
entityChangeIdReachedListeners . filter ( l => Date . now ( ) > l . start - 60000 )
2021-02-20 23:17:29 +01:00
. forEach ( l => console . log ( ` Waiting for entityChangeId ${ l . desiredEntityChangeId } while last processed is ${ lastProcessedEntityChangeId } (last accepted ${ lastAcceptedEntityChangeId } ) for ${ Math . floor ( ( Date . now ( ) - l . start ) / 1000 ) } s ` ) ) ;
2019-10-28 19:45:36 +01:00
}
2021-03-21 22:43:41 +01:00
async function consumeFrontendUpdateData ( ) {
if ( frontendUpdateDataQueue . length > 0 ) {
const allEntityChanges = frontendUpdateDataQueue ;
frontendUpdateDataQueue = [ ] ;
2019-10-20 17:49:58 +02:00
2020-12-16 22:17:42 +01:00
const nonProcessedEntityChanges = allEntityChanges . filter ( ec => ! processedEntityChangeIds . has ( ec . id ) ) ;
2020-05-14 13:08:06 +02:00
2019-12-16 22:47:07 +01:00
try {
2021-08-20 21:42:06 +02:00
await utils . timeLimit ( frocaUpdater . processEntityChanges ( nonProcessedEntityChanges ) , 30000 ) ;
2019-12-16 22:47:07 +01:00
}
catch ( e ) {
2020-02-05 22:08:45 +01:00
logError ( ` Encountered error ${ e . message } : ${ e . stack } , reloading frontend. ` ) ;
2019-12-02 22:27:06 +01:00
2020-09-18 23:22:28 +02:00
if ( ! glob . isDev && ! options . is ( 'debugModeEnabled' ) ) {
// if there's an error in updating the frontend then the easy option to recover is to reload the frontend completely
2021-08-24 22:59:51 +02:00
utils . reloadFrontendApp ( ) ;
2020-09-04 22:54:50 +02:00
}
2020-09-18 23:22:28 +02:00
else {
2020-12-14 14:17:51 +01:00
console . log ( "nonProcessedEntityChanges causing the timeout" , nonProcessedEntityChanges ) ;
2020-09-19 22:47:14 +02:00
2022-08-24 23:20:05 +02:00
toastService . showError ( ` Encountered error " ${ e . message } ", check out the console. ` ) ;
2020-09-18 23:22:28 +02:00
}
2019-12-16 22:47:07 +01:00
}
2019-10-20 17:49:58 +02:00
2020-12-14 14:17:51 +01:00
for ( const entityChange of nonProcessedEntityChanges ) {
processedEntityChangeIds . add ( entityChange . id ) ;
2020-05-30 22:35:18 +02:00
2021-11-16 22:12:53 +01:00
lastProcessedEntityChangeId = Math . max ( lastProcessedEntityChangeId , entityChange . id ) ;
}
2019-10-20 17:49:58 +02:00
}
2019-12-09 23:07:45 +01:00
2020-08-02 23:27:48 +02:00
checkEntityChangeIdListeners ( ) ;
2019-10-20 17:49:58 +02:00
}
2018-03-25 11:09:17 -04:00
function connectWebSocket ( ) {
2019-11-25 21:44:46 +01:00
const loc = window . location ;
const webSocketUri = ( loc . protocol === "https:" ? "wss:" : "ws:" )
+ "//" + loc . host + loc . pathname ;
2018-03-25 11:09:17 -04:00
// use wss for secure messaging
2019-11-25 21:44:46 +01:00
const ws = new WebSocket ( webSocketUri ) ;
ws . onopen = ( ) => console . debug ( utils . now ( ) , ` Connected to server ${ webSocketUri } with WebSocket ` ) ;
2018-03-25 21:16:57 -04:00
ws . onmessage = handleMessage ;
2019-07-06 12:03:51 +02:00
// we're not handling ws.onclose here because reconnection is done in sendPing()
2017-11-28 17:52:47 -05:00
2018-03-25 11:09:17 -04:00
return ws ;
}
2019-12-02 22:27:06 +01:00
async function sendPing ( ) {
if ( Date . now ( ) - lastPingTs > 30000 ) {
2020-05-11 22:44:10 +02:00
console . log ( utils . now ( ) , "Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket." ) ;
2019-12-02 22:27:06 +01:00
}
if ( ws . readyState === ws . OPEN ) {
ws . send ( JSON . stringify ( {
type : 'ping' ,
2020-08-02 23:27:48 +02:00
lastEntityChangeId : lastAcceptedEntityChangeId
2019-12-02 22:27:06 +01:00
} ) ) ;
}
else if ( ws . readyState === ws . CLOSED || ws . readyState === ws . CLOSING ) {
console . log ( utils . now ( ) , "WS closed or closing, trying to reconnect" ) ;
ws = connectWebSocket ( ) ;
}
}
2018-03-25 13:08:58 -04:00
setTimeout ( ( ) => {
ws = connectWebSocket ( ) ;
2019-02-10 16:36:25 +01:00
lastPingTs = Date . now ( ) ;
2018-03-25 13:08:58 -04:00
2019-12-02 22:27:06 +01:00
setInterval ( sendPing , 1000 ) ;
2018-04-05 23:17:19 -04:00
} , 0 ) ;
2017-12-01 22:28:22 -05:00
2018-03-25 11:09:17 -04:00
export default {
2018-03-25 21:16:57 -04:00
logError ,
2018-08-01 09:26:02 +02:00
subscribeToMessages ,
2021-03-21 22:43:41 +01:00
waitForMaxKnownEntityChangeId ,
getMaxKnownEntityChangeSyncId : ( ) => lastAcceptedEntityChangeSyncId
2020-05-11 22:44:10 +02:00
} ;