diff --git a/src/public/javascripts/services/frontend_script_api.js b/src/public/javascripts/services/frontend_script_api.js index ea6d2dd01..7d1711bda 100644 --- a/src/public/javascripts/services/frontend_script_api.js +++ b/src/public/javascripts/services/frontend_script_api.js @@ -382,6 +382,11 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte * @param {function} handler */ this.bindGlobalShortcut = utils.bindGlobalShortcut; + + /** + * @method + */ + this.waitUntilSynced = ws.waitForMaxKnownSyncId; } export default FrontendScriptApi; \ No newline at end of file diff --git a/src/public/javascripts/services/server.js b/src/public/javascripts/services/server.js index 721bb3849..b1c5f9949 100644 --- a/src/public/javascripts/services/server.js +++ b/src/public/javascripts/services/server.js @@ -42,12 +42,16 @@ async function remove(url, headers = {}) { let i = 1; const reqResolves = {}; +let maxKnownSyncId = 0; + async function call(method, url, data, headers = {}) { + let resp; + if (utils.isElectron()) { const ipc = require('electron').ipcRenderer; const requestId = i++; - return new Promise((resolve, reject) => { + resp = await new Promise((resolve, reject) => { reqResolves[requestId] = resolve; if (REQUEST_LOGGING_ENABLED) { @@ -64,32 +68,58 @@ async function call(method, url, data, headers = {}) { }); } else { - return await ajax(url, method, data, headers); + resp = await ajax(url, method, data, headers); } + + const maxSyncIdStr = resp.headers['trilium-max-sync-id']; + + if (maxSyncIdStr && maxSyncIdStr.trim()) { + maxKnownSyncId = Math.max(maxKnownSyncId, parseInt(maxSyncIdStr)); + } + + return resp.body; } -async function ajax(url, method, data, headers) { - const options = { - url: baseApiUrl + url, - type: method, - headers: getHeaders(headers), - timeout: 60000 - }; +function ajax(url, method, data, headers) { + return new Promise((res, rej) => { + const options = { + url: baseApiUrl + url, + type: method, + headers: getHeaders(headers), + timeout: 60000, + success: (body, textStatus, jqXhr) => { + const respHeaders = {}; - if (data) { - try { - options.data = JSON.stringify(data); - } - catch (e) { - console.log("Can't stringify data: ", data, " because of error: ", e) - } - options.contentType = "application/json"; - } + jqXhr.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach(line => { + const parts = line.split(': '); + const header = parts.shift(); + respHeaders[header] = parts.join(': '); + }); - return await $.ajax(options).catch(e => { - const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText; - toastService.showError(message); - toastService.throwError(message); + res({ + body, + headers: respHeaders + }); + }, + error: (jqXhr, textStatus, error) => { + const message = "Error when calling " + method + " " + url + ": " + textStatus + " - " + error; + toastService.showError(message); + toastService.throwError(message); + + rej(error); + } + }; + + if (data) { + try { + options.data = JSON.stringify(data); + } catch (e) { + console.log("Can't stringify data: ", data, " because of error: ", e) + } + options.contentType = "application/json"; + } + + $.ajax(options); }); } @@ -101,7 +131,10 @@ if (utils.isElectron()) { console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode); } - reqResolves[arg.requestId](arg.body); + reqResolves[arg.requestId]({ + body: arg.body, + headers: arg.headers + }); delete reqResolves[arg.requestId]; }); @@ -114,5 +147,6 @@ export default { remove, ajax, // don't remove, used from CKEditor image upload! - getHeaders + getHeaders, + getMaxKnownSyncId: () => maxKnownSyncId }; \ No newline at end of file diff --git a/src/public/javascripts/services/utils.js b/src/public/javascripts/services/utils.js index 7e9beb642..4436232df 100644 --- a/src/public/javascripts/services/utils.js +++ b/src/public/javascripts/services/utils.js @@ -77,7 +77,7 @@ async function stopWatch(what, func) { } function formatValueWithWhitespace(val) { - return /[^\p{L}_-]/u.test(val) ? '"' + val + '"' : val; + return /[^\w_-]/.test(val) ? '"' + val + '"' : val; } function formatLabel(label) { diff --git a/src/public/javascripts/services/ws.js b/src/public/javascripts/services/ws.js index 7e90e23dc..463c1813c 100644 --- a/src/public/javascripts/services/ws.js +++ b/src/public/javascripts/services/ws.js @@ -1,5 +1,6 @@ import utils from './utils.js'; import toastService from "./toast.js"; +import server from "./server.js"; const $outstandingSyncsCount = $("#outstanding-syncs-count"); @@ -71,8 +72,6 @@ async function handleMessage(event) { // finish and set to null to signal somebody else can pick it up consumeQueuePromise = null; } - - checkSyncIdListeners(); } else if (message.type === 'sync-hash-check-failed') { toastService.showError("Sync check failed!", 60000); @@ -98,6 +97,10 @@ function waitForSyncId(desiredSyncId) { }); } +function waitForMaxKnownSyncId() { + return waitForSyncId(server.getMaxKnownSyncId()); +} + function checkSyncIdListeners() { syncIdReachedListeners .filter(l => l.desiredSyncId <= lastProcessedSyncId) @@ -129,6 +132,8 @@ async function consumeSyncData() { lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncData[allSyncData.length - 1].id); } + + checkSyncIdListeners(); } function connectWebSocket() { @@ -193,5 +198,6 @@ export default { subscribeToMessages, subscribeToAllSyncMessages, subscribeToOutsideSyncMessages, - waitForSyncId + waitForSyncId, + waitForMaxKnownSyncId }; \ No newline at end of file diff --git a/src/routes/electron.js b/src/routes/electron.js index 3f1894be7..3589382d2 100644 --- a/src/routes/electron.js +++ b/src/routes/electron.js @@ -12,10 +12,14 @@ function init(app) { } }; + const respHeaders = {}; + const res = { statusCode: 200, - getHeader: () => {}, - setHeader: () => {}, + getHeader: name => respHeaders[name], + setHeader: (name, value) => { + respHeaders[name] = value.toString(); + }, status: statusCode => { res.statusCode = statusCode; return res; @@ -24,6 +28,7 @@ function init(app) { event.sender.send('server-response', { requestId: arg.requestId, statusCode: res.statusCode, + headers: respHeaders, body: obj }); } diff --git a/src/routes/routes.js b/src/routes/routes.js index 6a4f0d408..eca807938 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -44,6 +44,7 @@ const auth = require('../services/auth'); const cls = require('../services/cls'); const sql = require('../services/sql'); const protectedSessionService = require('../services/protected_session'); +const syncTableService = require('../services/sync_table'); const csurf = require('csurf'); const csrfMiddleware = csurf({ @@ -52,6 +53,8 @@ const csrfMiddleware = csurf({ }); function apiResultHandler(req, res, result) { + res.setHeader('trilium-max-sync-id', syncTableService.getMaxSyncId()); + // if it's an array and first element is integer then we consider this to be [statusCode, response] format if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) { const [statusCode, response] = result; diff --git a/src/services/sync_table.js b/src/services/sync_table.js index 687368442..ffb95b639 100644 --- a/src/services/sync_table.js +++ b/src/services/sync_table.js @@ -21,6 +21,10 @@ async function addEntitySync(entityName, entityId, sourceId) { setTimeout(() => require('./ws').sendPingToAllClients(), 50); } +function getMaxSyncId() { + return syncs.length === 0 ? 0 : syncs[syncs.length - 1].id; +} + function getEntitySyncsNewerThan(syncId) { return syncs.filter(s => s.id > syncId); } @@ -96,5 +100,6 @@ module.exports = { addApiTokenSync: async (apiTokenId, sourceId) => await addEntitySync("api_tokens", apiTokenId, sourceId), addEntitySync, fillAllSyncRows, - getEntitySyncsNewerThan + getEntitySyncsNewerThan, + getMaxSyncId }; \ No newline at end of file