add memberId to entity_changes to avoid having to resend all changes second time

This commit is contained in:
zadam 2022-01-09 20:16:39 +01:00
parent c448d34a38
commit 7159b13c9d
25 changed files with 11520 additions and 266 deletions

View File

@ -44,7 +44,7 @@ find $DIR/node_modules -name demo -exec rm -rf {} \;
find $DIR/libraries -name "*.map" -type f -delete find $DIR/libraries -name "*.map" -type f -delete
rm -r $DIR/src/public/app rm -rf $DIR/src/public/app
sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs
sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs

View File

@ -5,7 +5,8 @@ CREATE TABLE IF NOT EXISTS "entity_changes" (
`hash` TEXT NOT NULL, `hash` TEXT NOT NULL,
`isErased` INT NOT NULL, `isErased` INT NOT NULL,
`changeId` TEXT NOT NULL, `changeId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL, `componentId` TEXT NOT NULL,
`memberId` TEXT NOT NULL,
`isSynced` INTEGER NOT NULL, `isSynced` INTEGER NOT NULL,
`utcDateChanged` TEXT NOT NULL `utcDateChanged` TEXT NOT NULL
); );

11484
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -43,10 +43,10 @@
"@electron/remote": "2.0.1", "@electron/remote": "2.0.1",
"express": "4.17.2", "express": "4.17.2",
"express-partial-content": "^1.0.2", "express-partial-content": "^1.0.2",
"express-rate-limit": "5.5.1", "express-rate-limit": "6.0.5",
"express-session": "1.17.2", "express-session": "1.17.2",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"helmet": "4.6.0", "helmet": "5.0.1",
"html": "1.0.0", "html": "1.0.0",
"html2plaintext": "2.1.4", "html2plaintext": "2.1.4",
"http-proxy-agent": "5.0.0", "http-proxy-agent": "5.0.0",
@ -88,7 +88,7 @@
"electron-packager": "15.4.0", "electron-packager": "15.4.0",
"electron-rebuild": "3.2.5", "electron-rebuild": "3.2.5",
"esm": "3.2.25", "esm": "3.2.25",
"jasmine": "3.10.0", "jasmine": "4.0.1",
"jsdoc": "3.6.7", "jsdoc": "3.6.7",
"lorem-ipsum": "2.0.4", "lorem-ipsum": "2.0.4",
"rcedit": "3.0.1", "rcedit": "3.0.1",

View File

@ -41,7 +41,7 @@ function route(router, method, path, routeHandler) {
cls.namespace.bindEmitter(res); cls.namespace.bindEmitter(res);
cls.init(() => { cls.init(() => {
cls.set('sourceId', "etapi"); cls.set('componentId', "etapi");
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
const cb = () => routeHandler(req, res, next); const cb = () => routeHandler(req, res, next);

View File

@ -22,9 +22,9 @@ async function processEntityChanges(entityChanges) {
} else if (ec.entityName === 'note_contents') { } else if (ec.entityName === 'note_contents') {
delete froca.noteComplementPromises[ec.entityId]; delete froca.noteComplementPromises[ec.entityId];
loadResults.addNoteContent(ec.entityId, ec.sourceId); loadResults.addNoteContent(ec.entityId, ec.componentId);
} else if (ec.entityName === 'note_revisions') { } else if (ec.entityName === 'note_revisions') {
loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.sourceId); loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.componentId);
} else if (ec.entityName === 'note_revision_contents') { } else if (ec.entityName === 'note_revision_contents') {
// this should change only when toggling isProtected, ignore // this should change only when toggling isProtected, ignore
} else if (ec.entityName === 'options') { } else if (ec.entityName === 'options') {
@ -87,7 +87,7 @@ function processNoteChange(loadResults, ec) {
return; return;
} }
loadResults.addNote(ec.entityId, ec.sourceId); loadResults.addNote(ec.entityId, ec.componentId);
if (ec.isErased && ec.entityId in froca.notes) { if (ec.isErased && ec.entityId in froca.notes) {
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
@ -125,7 +125,7 @@ function processBranchChange(loadResults, ec) {
delete parentNote.childToBranch[branch.noteId]; delete parentNote.childToBranch[branch.noteId];
} }
loadResults.addBranch(ec.entityId, ec.sourceId); loadResults.addBranch(ec.entityId, ec.componentId);
delete froca.branches[ec.entityId]; delete froca.branches[ec.entityId];
} }
@ -133,7 +133,7 @@ function processBranchChange(loadResults, ec) {
return; return;
} }
loadResults.addBranch(ec.entityId, ec.sourceId); loadResults.addBranch(ec.entityId, ec.componentId);
const childNote = froca.notes[ec.entity.noteId]; const childNote = froca.notes[ec.entity.noteId];
const parentNote = froca.notes[ec.entity.parentNoteId]; const parentNote = froca.notes[ec.entity.parentNoteId];
@ -175,7 +175,7 @@ function processNoteReordering(loadResults, ec) {
} }
} }
loadResults.addNoteReordering(ec.entityId, ec.sourceId); loadResults.addNoteReordering(ec.entityId, ec.componentId);
} }
function processAttributeChange(loadResults, ec) { function processAttributeChange(loadResults, ec) {
@ -199,7 +199,7 @@ function processAttributeChange(loadResults, ec) {
targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.attributeId); targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.attributeId);
} }
loadResults.addAttribute(ec.entityId, ec.sourceId); loadResults.addAttribute(ec.entityId, ec.componentId);
delete froca.attributes[ec.entityId]; delete froca.attributes[ec.entityId];
} }
@ -207,7 +207,7 @@ function processAttributeChange(loadResults, ec) {
return; return;
} }
loadResults.addAttribute(ec.entityId, ec.sourceId); loadResults.addAttribute(ec.entityId, ec.componentId);
const sourceNote = froca.notes[ec.entity.noteId]; const sourceNote = froca.notes[ec.entity.noteId];
const targetNote = ec.entity.type === 'relation' && froca.notes[ec.entity.value]; const targetNote = ec.entity.type === 'relation' && froca.notes[ec.entity.value];

View File

@ -9,8 +9,8 @@ export default class LoadResults {
} }
} }
this.noteIdToSourceId = {}; this.noteIdToComponentId = {};
this.sourceIdToNoteIds = {}; this.componentIdToNoteIds = {};
this.branches = []; this.branches = [];
@ -20,7 +20,7 @@ export default class LoadResults {
this.noteRevisions = []; this.noteRevisions = [];
this.contentNoteIdToSourceId = []; this.contentNoteIdToComponentId = [];
this.options = []; this.options = [];
} }
@ -29,22 +29,22 @@ export default class LoadResults {
return this.entities[entityName]?.[entityId]; return this.entities[entityName]?.[entityId];
} }
addNote(noteId, sourceId) { addNote(noteId, componentId) {
this.noteIdToSourceId[noteId] = this.noteIdToSourceId[noteId] || []; this.noteIdToComponentId[noteId] = this.noteIdToComponentId[noteId] || [];
if (!this.noteIdToSourceId[noteId].includes(sourceId)) { if (!this.noteIdToComponentId[noteId].includes(componentId)) {
this.noteIdToSourceId[noteId].push(sourceId); this.noteIdToComponentId[noteId].push(componentId);
} }
this.sourceIdToNoteIds[sourceId] = this.sourceIdToNoteIds[sourceId] || []; this.componentIdToNoteIds[componentId] = this.componentIdToNoteIds[componentId] || [];
if (!this.sourceIdToNoteIds[sourceId]) { if (!this.componentIdToNoteIds[componentId]) {
this.sourceIdToNoteIds[sourceId].push(noteId); this.componentIdToNoteIds[componentId].push(noteId);
} }
} }
addBranch(branchId, sourceId) { addBranch(branchId, componentId) {
this.branches.push({branchId, sourceId}); this.branches.push({branchId, componentId});
} }
getBranches() { getBranches() {
@ -53,7 +53,7 @@ export default class LoadResults {
.filter(branch => !!branch); .filter(branch => !!branch);
} }
addNoteReordering(parentNoteId, sourceId) { addNoteReordering(parentNoteId, componentId) {
this.noteReorderings.push(parentNoteId); this.noteReorderings.push(parentNoteId);
} }
@ -61,20 +61,20 @@ export default class LoadResults {
return this.noteReorderings; return this.noteReorderings;
} }
addAttribute(attributeId, sourceId) { addAttribute(attributeId, componentId) {
this.attributes.push({attributeId, sourceId}); this.attributes.push({attributeId, componentId});
} }
/** @returns {Attribute[]} */ /** @returns {Attribute[]} */
getAttributes(sourceId = 'none') { getAttributes(componentId = 'none') {
return this.attributes return this.attributes
.filter(row => row.sourceId !== sourceId) .filter(row => row.componentId !== componentId)
.map(row => this.getEntity("attributes", row.attributeId)) .map(row => this.getEntity("attributes", row.attributeId))
.filter(attr => !!attr); .filter(attr => !!attr);
} }
addNoteRevision(noteRevisionId, noteId, sourceId) { addNoteRevision(noteRevisionId, noteId, componentId) {
this.noteRevisions.push({noteRevisionId, noteId, sourceId}); this.noteRevisions.push({noteRevisionId, noteId, componentId});
} }
hasNoteRevisionForNote(noteId) { hasNoteRevisionForNote(noteId) {
@ -82,28 +82,28 @@ export default class LoadResults {
} }
getNoteIds() { getNoteIds() {
return Object.keys(this.noteIdToSourceId); return Object.keys(this.noteIdToComponentId);
} }
isNoteReloaded(noteId, sourceId = null) { isNoteReloaded(noteId, componentId = null) {
if (!noteId) { if (!noteId) {
return false; return false;
} }
const sourceIds = this.noteIdToSourceId[noteId]; const componentIds = this.noteIdToComponentId[noteId];
return sourceIds && !!sourceIds.find(sId => sId !== sourceId); return componentIds && !!componentIds.find(sId => sId !== componentId);
} }
addNoteContent(noteId, sourceId) { addNoteContent(noteId, componentId) {
this.contentNoteIdToSourceId.push({noteId, sourceId}); this.contentNoteIdToComponentId.push({noteId, componentId});
} }
isNoteContentReloaded(noteId, sourceId) { isNoteContentReloaded(noteId, componentId) {
if (!noteId) { if (!noteId) {
return false; return false;
} }
return this.contentNoteIdToSourceId.find(l => l.noteId === noteId && l.sourceId !== sourceId); return this.contentNoteIdToComponentId.find(l => l.noteId === noteId && l.componentId !== componentId);
} }
addOption(name) { addOption(name) {
@ -124,17 +124,17 @@ export default class LoadResults {
} }
isEmpty() { isEmpty() {
return Object.keys(this.noteIdToSourceId).length === 0 return Object.keys(this.noteIdToComponentId).length === 0
&& this.branches.length === 0 && this.branches.length === 0
&& this.attributes.length === 0 && this.attributes.length === 0
&& this.noteReorderings.length === 0 && this.noteReorderings.length === 0
&& this.noteRevisions.length === 0 && this.noteRevisions.length === 0
&& this.contentNoteIdToSourceId.length === 0 && this.contentNoteIdToComponentId.length === 0
&& this.options.length === 0; && this.options.length === 0;
} }
isEmptyForTree() { isEmptyForTree() {
return Object.keys(this.noteIdToSourceId).length === 0 return Object.keys(this.noteIdToComponentId).length === 0
&& this.branches.length === 0 && this.branches.length === 0
&& this.attributes.length === 0 && this.attributes.length === 0
&& this.noteReorderings.length === 0; && this.noteReorderings.length === 0;

View File

@ -9,7 +9,7 @@ async function getHeaders(headers) {
// headers need to be lowercase because node.js automatically converts them to lower case // headers need to be lowercase because node.js automatically converts them to lower case
// also avoiding using underscores instead of dashes since nginx filters them out by default // also avoiding using underscores instead of dashes since nginx filters them out by default
const allHeaders = { const allHeaders = {
'trilium-source-id': glob.sourceId, 'trilium-component-id': glob.componentId,
'trilium-local-now-datetime': utils.localNowDateTime(), 'trilium-local-now-datetime': utils.localNowDateTime(),
'trilium-hoisted-note-id': activeNoteContext ? activeNoteContext.hoistedNoteId : null, 'trilium-hoisted-note-id': activeNoteContext ? activeNoteContext.hoistedNoteId : null,
'x-csrf-token': glob.csrfToken 'x-csrf-token': glob.csrfToken
@ -29,20 +29,20 @@ async function getHeaders(headers) {
return allHeaders; return allHeaders;
} }
async function get(url, sourceId) { async function get(url, componentId) {
return await call('GET', url, null, {'trilium-source-id': sourceId}); return await call('GET', url, null, {'trilium-component-id': componentId});
} }
async function post(url, data, sourceId) { async function post(url, data, componentId) {
return await call('POST', url, data, {'trilium-source-id': sourceId}); return await call('POST', url, data, {'trilium-component-id': componentId});
} }
async function put(url, data, sourceId) { async function put(url, data, componentId) {
return await call('PUT', url, data, {'trilium-source-id': sourceId}); return await call('PUT', url, data, {'trilium-component-id': componentId});
} }
async function remove(url, sourceId) { async function remove(url, componentId) {
return await call('DELETE', url, null, {'trilium-source-id': sourceId}); return await call('DELETE', url, null, {'trilium-component-id': componentId});
} }
let i = 1; let i = 1;

View File

@ -21,6 +21,7 @@ function SetupModel() {
this.syncServerHost = ko.observable(); this.syncServerHost = ko.observable();
this.syncProxy = ko.observable(); this.syncProxy = ko.observable();
this.password = ko.observable();
this.instanceType = utils.isElectron() ? "desktop" : "server"; this.instanceType = utils.isElectron() ? "desktop" : "server";
@ -48,19 +49,13 @@ function SetupModel() {
this.finish = async () => { this.finish = async () => {
const syncServerHost = this.syncServerHost(); const syncServerHost = this.syncServerHost();
const syncProxy = this.syncProxy(); const syncProxy = this.syncProxy();
const username = this.username(); const password = this.password();
const password = this.password1();
if (!syncServerHost) { if (!syncServerHost) {
showAlert("Trilium server address can't be empty"); showAlert("Trilium server address can't be empty");
return; return;
} }
if (!username) {
showAlert("Username can't be empty");
return;
}
if (!password) { if (!password) {
showAlert("Password can't be empty"); showAlert("Password can't be empty");
return; return;
@ -70,7 +65,6 @@ function SetupModel() {
const resp = await $.post('api/setup/sync-from-server', { const resp = await $.post('api/setup/sync-from-server', {
syncServerHost: syncServerHost, syncServerHost: syncServerHost,
syncProxy: syncProxy, syncProxy: syncProxy,
username: username,
password: password password: password
}); });

View File

@ -3,7 +3,7 @@
const options = require('../../services/options'); const options = require('../../services/options');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const sourceIdService = require('../../services/source_id'); const memberId = require('../../services/member_id');
const passwordEncryptionService = require('../../services/password_encryption'); const passwordEncryptionService = require('../../services/password_encryption');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');
@ -47,7 +47,7 @@ function loginSync(req) {
req.session.loggedIn = true; req.session.loggedIn = true;
return { return {
sourceId: sourceIdService.getCurrentSourceId(), memberId: memberId,
maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1") maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1")
}; };
} }

View File

@ -123,13 +123,36 @@ function forceNoteSync(req) {
function getChanged(req) { function getChanged(req) {
const startTime = Date.now(); const startTime = Date.now();
const lastEntityChangeId = parseInt(req.query.lastEntityChangeId); let lastEntityChangeId = parseInt(req.query.lastEntityChangeId);
const clientMemberId = req.query.memberId;
let filteredEntityChanges = [];
const entityChanges = sql.getRows("SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastEntityChangeId]); while (filteredEntityChanges.length === 0) {
const entityChanges = sql.getRows(`
SELECT *
FROM entity_changes
WHERE isSynced = 1
AND id > ?
ORDER BY id
LIMIT 1000`, [lastEntityChangeId]);
if (entityChanges.length === 0) {
break;
}
filteredEntityChanges = entityChanges.filter(ec => ec.memberId !== clientMemberId);
}
const entityChangesRecords = syncService.getEntityChangesRecords(filteredEntityChanges);
if (entityChangesRecords.length > 0) {
lastEntityChangeId = entityChangesRecords[entityChangesRecords.length - 1].entityChange.id;
}
const ret = { const ret = {
entityChanges: syncService.getEntityChangesRecords(entityChanges), entityChanges: entityChangesRecords,
maxEntityChangeId: sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1') maxEntityChangeId: sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1'),
lastEntityChangeId
}; };
if (ret.entityChanges.length > 0) { if (ret.entityChanges.length > 0) {
@ -174,10 +197,10 @@ function update(req) {
} }
} }
const {entities} = body; const {entities, memberId} = body;
for (const {entityChange, entity} of entities) { for (const {entityChange, entity} of entities) {
syncUpdateService.updateEntity(entityChange, entity); syncUpdateService.updateEntity(entityChange, entity, memberId);
} }
} }

View File

@ -1,6 +1,5 @@
"use strict"; "use strict";
const sourceIdService = require('../services/source_id');
const sql = require('../services/sql'); const sql = require('../services/sql');
const attributeService = require('../services/attributes'); const attributeService = require('../services/attributes');
const config = require('../services/config'); const config = require('../services/config');
@ -28,7 +27,6 @@ function index(req, res) {
mainFontSize: parseInt(options.mainFontSize), mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize), treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize), detailFontSize: parseInt(options.detailFontSize),
sourceId: sourceIdService.generateSourceId(),
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"), maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"),
maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"), maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"),
instanceName: config.General ? config.General.instanceName : null, instanceName: config.General ? config.General.instanceName : null,

View File

@ -144,7 +144,7 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
cls.namespace.bindEmitter(res); cls.namespace.bindEmitter(res);
const result = cls.init(() => { const result = cls.init(() => {
cls.set('sourceId', req.headers['trilium-source-id']); cls.set('componentId', req.headers['trilium-component-id']);
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root'); cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root');

View File

@ -4,7 +4,7 @@ const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir'); const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 190; const APP_DB_VERSION = 191;
const SYNC_VERSION = 25; const SYNC_VERSION = 25;
const CLIPPER_PROTOCOL_VERSION = "1.0"; const CLIPPER_PROTOCOL_VERSION = "1.0";

View File

@ -28,8 +28,8 @@ function getHoistedNoteId() {
return namespace.get('hoistedNoteId') || 'root'; return namespace.get('hoistedNoteId') || 'root';
} }
function getSourceId() { function getComponentId() {
return namespace.get('sourceId'); return namespace.get('componentId');
} }
function getLocalNowDateTime() { function getLocalNowDateTime() {
@ -80,7 +80,7 @@ module.exports = {
set, set,
namespace, namespace,
getHoistedNoteId, getHoistedNoteId,
getSourceId, getComponentId,
getLocalNowDateTime, getLocalNowDateTime,
disableEntityEvents, disableEntityEvents,
isEntityEventsDisabled, isEntityEventsDisabled,

View File

@ -1,13 +1,19 @@
const sql = require('./sql'); const sql = require('./sql');
const sourceIdService = require('./source_id');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const log = require('./log'); const log = require('./log');
const cls = require('./cls'); const cls = require('./cls');
const utils = require('./utils'); const utils = require('./utils');
const memberId = require('./member_id');
const becca = require("../becca/becca"); const becca = require("../becca/becca");
let maxEntityChangeId = 0; let maxEntityChangeId = 0;
function addEntityChangeWithMemberId(origEntityChange, memberId) {
const ec = {...origEntityChange, memberId};
return addEntityChange(ec);
}
function addEntityChange(origEntityChange) { function addEntityChange(origEntityChange) {
const ec = {...origEntityChange}; const ec = {...origEntityChange};
@ -17,7 +23,8 @@ function addEntityChange(origEntityChange) {
ec.changeId = utils.randomString(12); ec.changeId = utils.randomString(12);
} }
ec.sourceId = ec.sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId(); ec.componentId = ec.componentId || cls.getComponentId() || "";
ec.memberId = ec.memberId || memberId;
ec.isSynced = ec.isSynced ? 1 : 0; ec.isSynced = ec.isSynced ? 1 : 0;
ec.isErased = ec.isErased ? 1 : 0; ec.isErased = ec.isErased ? 1 : 0;
ec.id = sql.replace("entity_changes", ec); ec.id = sql.replace("entity_changes", ec);
@ -27,7 +34,7 @@ function addEntityChange(origEntityChange) {
cls.addEntityChange(ec); cls.addEntityChange(ec);
} }
function addNoteReorderingEntityChange(parentNoteId, sourceId) { function addNoteReorderingEntityChange(parentNoteId, componentId) {
addEntityChange({ addEntityChange({
entityName: "note_reordering", entityName: "note_reordering",
entityId: parentNoteId, entityId: parentNoteId,
@ -35,7 +42,8 @@ function addNoteReorderingEntityChange(parentNoteId, sourceId) {
isErased: false, isErased: false,
utcDateChanged: dateUtils.utcNowDateTime(), utcDateChanged: dateUtils.utcNowDateTime(),
isSynced: true, isSynced: true,
sourceId componentId,
memberId: memberId
}); });
const eventService = require('./events'); const eventService = require('./events');
@ -138,6 +146,7 @@ module.exports = {
addNoteReorderingEntityChange, addNoteReorderingEntityChange,
moveEntityChangeToTop, moveEntityChangeToTop,
addEntityChange, addEntityChange,
addEntityChangeWithMemberId,
fillAllEntityChanges, fillAllEntityChanges,
addEntityChangesForSector, addEntityChangesForSector,
getMaxEntityChangeId: () => maxEntityChangeId getMaxEntityChangeId: () => maxEntityChangeId

View File

@ -0,0 +1,5 @@
const utils = require('./utils');
const memberId = utils.randomString(12);
module.exports = memberId;

View File

@ -30,9 +30,9 @@ function executeBundle(bundle, apiParams = {}) {
apiParams.startNote = bundle.note; apiParams.startNote = bundle.note;
} }
const originalSourceId = cls.get('sourceId'); const originalComponentId = cls.get('componentId');
cls.set('sourceId', 'script'); cls.set('componentId', 'script');
// last \r\n is necessary if script contains line comment on its last line // last \r\n is necessary if script contains line comment on its last line
const script = "function() {\r\n" + bundle.script + "\r\n}"; const script = "function() {\r\n" + bundle.script + "\r\n}";
@ -47,7 +47,7 @@ function executeBundle(bundle, apiParams = {}) {
throw e; throw e;
} }
finally { finally {
cls.set('sourceId', originalSourceId); cls.set('componentId', originalComponentId);
} }
} }

View File

@ -1,27 +0,0 @@
const utils = require('./utils');
const localSourceIds = {};
function generateSourceId() {
const sourceId = utils.randomString(12);
localSourceIds[sourceId] = true;
return sourceId;
}
function isLocalSourceId(srcId) {
return !!localSourceIds[srcId];
}
const currentSourceId = generateSourceId();
function getCurrentSourceId() {
return currentSourceId;
}
module.exports = {
generateSourceId,
getCurrentSourceId,
isLocalSourceId
};

View File

@ -4,7 +4,7 @@ const log = require('./log');
const sql = require('./sql'); const sql = require('./sql');
const optionService = require('./options'); const optionService = require('./options');
const utils = require('./utils'); const utils = require('./utils');
const sourceIdService = require('./source_id'); const memberId = require('./member_id');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update'); const syncUpdateService = require('./sync_update');
const contentHashService = require('./content_hash'); const contentHashService = require('./content_hash');
@ -107,11 +107,11 @@ async function doLogin() {
hash: hash hash: hash
}); });
if (sourceIdService.isLocalSourceId(resp.sourceId)) { if (resp.memberId === memberId) {
throw new Error(`Sync server has source ID ${resp.sourceId} which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`); throw new Error(`Sync server has member ID ${resp.memberId} which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`);
} }
syncContext.sourceId = resp.sourceId; syncContext.memberId = resp.memberId;
const lastSyncedPull = getLastSyncedPull(); const lastSyncedPull = getLastSyncedPull();
@ -131,7 +131,7 @@ async function pullChanges(syncContext) {
while (true) { while (true) {
const lastSyncedPull = getLastSyncedPull(); const lastSyncedPull = getLastSyncedPull();
const changesUri = '/api/sync/changed?lastEntityChangeId=' + lastSyncedPull; const changesUri = `/api/sync/changed?memberId=${memberId}&lastEntityChangeId=${lastSyncedPull}`;
const startDate = Date.now(); const startDate = Date.now();
@ -141,37 +141,39 @@ async function pullChanges(syncContext) {
outstandingPullCount = Math.max(0, resp.maxEntityChangeId - lastSyncedPull); outstandingPullCount = Math.max(0, resp.maxEntityChangeId - lastSyncedPull);
const {entityChanges} = resp; const {entityChanges, lastEntityChangeId} = resp;
if (entityChanges.length === 0) {
break;
}
sql.transactional(() => { sql.transactional(() => {
for (const {entityChange, entity} of entityChanges) { for (const {entityChange, entity} of entityChanges) {
const changeAppliedAlready = !entityChange.changeId const changeAppliedAlready = !entityChange.changeId
|| !!sql.getValue("SELECT id FROM entity_changes WHERE changeId = ?", [entityChange.changeId]); || !!sql.getValue("SELECT id FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
if (!changeAppliedAlready && !sourceIdService.isLocalSourceId(entityChange.sourceId)) { if (!changeAppliedAlready) {
if (!atLeastOnePullApplied) { // send only for first if (!atLeastOnePullApplied) { // send only for first
ws.syncPullInProgress(); ws.syncPullInProgress();
atLeastOnePullApplied = true; atLeastOnePullApplied = true;
} }
syncUpdateService.updateEntity(entityChange, entity); syncUpdateService.updateEntity(entityChange, entity, syncContext.memberId);
} }
outstandingPullCount = Math.max(0, resp.maxEntityChangeId - entityChange.id); outstandingPullCount = Math.max(0, resp.maxEntityChangeId - entityChange.id);
} }
setLastSyncedPull(entityChanges[entityChanges.length - 1].entityChange.id); if (lastSyncedPull !== lastEntityChangeId) {
setLastSyncedPull(lastEntityChangeId);
}
}); });
if (entityChanges.length === 0) {
break;
} else {
const sizeInKb = Math.round(JSON.stringify(resp).length / 1024); const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
log.info(`Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`); log.info(`Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
} }
}
log.info("Finished pull"); log.info("Finished pull");
} }
@ -189,10 +191,9 @@ async function pushChanges(syncContext) {
} }
const filteredEntityChanges = entityChanges.filter(entityChange => { const filteredEntityChanges = entityChanges.filter(entityChange => {
if (entityChange.sourceId === syncContext.sourceId) { if (entityChange.memberId === syncContext.memberId) {
// this may set lastSyncedPush beyond what's actually sent (because of size limit) // this may set lastSyncedPush beyond what's actually sent (because of size limit)
// so this is applied to the database only if there's no actual update // so this is applied to the database only if there's no actual update
// TODO: it would be better to simplify this somehow
lastSyncedPush = entityChange.id; lastSyncedPush = entityChange.id;
return false; return false;
@ -214,8 +215,8 @@ async function pushChanges(syncContext) {
const startDate = new Date(); const startDate = new Date();
await syncRequest(syncContext, 'PUT', '/api/sync/update', { await syncRequest(syncContext, 'PUT', '/api/sync/update', {
sourceId: sourceIdService.getCurrentSourceId(), entities: entityChangesRecords,
entities: entityChangesRecords memberId
}); });
ws.syncPushInProgress(); ws.syncPushInProgress();

View File

@ -4,12 +4,12 @@ const entityChangesService = require('./entity_changes');
const eventService = require('./events'); const eventService = require('./events');
const entityConstructor = require("../becca/entity_constructor"); const entityConstructor = require("../becca/entity_constructor");
function updateEntity(entityChange, entityRow) { function updateEntity(entityChange, entityRow, memberId) {
// can be undefined for options with isSynced=false // can be undefined for options with isSynced=false
if (!entityRow) { if (!entityRow) {
if (entityChange.isSynced) { if (entityChange.isSynced) {
if (entityChange.isErased) { if (entityChange.isErased) {
eraseEntity(entityChange); eraseEntity(entityChange, memberId);
} }
else { else {
log.info(`Encountered synced non-erased entity change without entity: ${JSON.stringify(entityChange)}`); log.info(`Encountered synced non-erased entity change without entity: ${JSON.stringify(entityChange)}`);
@ -23,8 +23,8 @@ function updateEntity(entityChange, entityRow) {
} }
const updated = entityChange.entityName === 'note_reordering' const updated = entityChange.entityName === 'note_reordering'
? updateNoteReordering(entityChange, entityRow) ? updateNoteReordering(entityChange, entityRow, memberId)
: updateNormalEntity(entityChange, entityRow); : updateNormalEntity(entityChange, entityRow, memberId);
if (updated) { if (updated) {
if (entityRow.isDeleted) { if (entityRow.isDeleted) {
@ -42,7 +42,7 @@ function updateEntity(entityChange, entityRow) {
} }
} }
function updateNormalEntity(remoteEntityChange, entity) { function updateNormalEntity(remoteEntityChange, entity, memberId) {
const localEntityChange = sql.getRow(` const localEntityChange = sql.getRow(`
SELECT utcDateChanged, hash, isErased SELECT utcDateChanged, hash, isErased
FROM entity_changes FROM entity_changes
@ -54,7 +54,7 @@ function updateNormalEntity(remoteEntityChange, entity) {
sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId); sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId);
entityChangesService.addEntityChange(remoteEntityChange); entityChangesService.addEntityChangeWithMemberId(remoteEntityChange, memberId);
}); });
return true; return true;
@ -71,7 +71,7 @@ function updateNormalEntity(remoteEntityChange, entity) {
sql.transactional(() => { sql.transactional(() => {
sql.replace(remoteEntityChange.entityName, entity); sql.replace(remoteEntityChange.entityName, entity);
entityChangesService.addEntityChange(remoteEntityChange); entityChangesService.addEntityChangeWithMemberId(remoteEntityChange, memberId);
}); });
return true; return true;
@ -80,13 +80,13 @@ function updateNormalEntity(remoteEntityChange, entity) {
return false; return false;
} }
function updateNoteReordering(entityChange, entity) { function updateNoteReordering(entityChange, entity, memberId) {
sql.transactional(() => { sql.transactional(() => {
for (const key in entity) { for (const key in entity) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]); sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
} }
entityChangesService.addEntityChange(entityChange); entityChangesService.addEntityChangeWithMemberId(entityChange, memberId);
}); });
return true; return true;
@ -105,7 +105,7 @@ function handleContent(content) {
return content; return content;
} }
function eraseEntity(entityChange) { function eraseEntity(entityChange, memberId) {
const {entityName, entityId} = entityChange; const {entityName, entityId} = entityChange;
if (!["notes", "note_contents", "branches", "attributes", "note_revisions", "note_revision_contents"].includes(entityName)) { if (!["notes", "note_contents", "branches", "attributes", "note_revisions", "note_revision_contents"].includes(entityName)) {
@ -119,7 +119,7 @@ function eraseEntity(entityChange) {
eventService.emit(eventService.ENTITY_DELETE_SYNCED, { entityName, entityId }); eventService.emit(eventService.ENTITY_DELETE_SYNCED, { entityName, entityId });
entityChangesService.addEntityChange(entityChange, true); entityChangesService.addEntityChangeWithMemberId(entityChange, memberId);
} }
module.exports = { module.exports = {

View File

@ -48,7 +48,6 @@
window.device = "desktop"; window.device = "desktop";
window.glob = { window.glob = {
activeDialog: null, activeDialog: null,
sourceId: '<%= sourceId %>',
maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>, maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>,
maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>, maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>,
instanceName: '<%= instanceName %>', instanceName: '<%= instanceName %>',

View File

@ -111,7 +111,6 @@
window.device = "mobile"; window.device = "mobile";
window.glob = { window.glob = {
activeDialog: null, activeDialog: null,
sourceId: '<%= sourceId %>',
maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>, maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>,
maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>, maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>,
instanceName: '<%= instanceName %>', instanceName: '<%= instanceName %>',

View File

@ -119,8 +119,8 @@
<p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p> <p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password1">Password</label> <label for="password">Password</label>
<input type="password" id="password1" class="form-control" data-bind="value: password1" placeholder="Password"> <input type="password" id="password" class="form-control" data-bind="value: password" placeholder="Password">
</div> </div>
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button> <button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
@ -146,7 +146,7 @@
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */ global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = { window.glob = {
sourceId: '' componentId: ''
}; };
window.syncInProgress = <%= syncInProgress ? 'true' : 'false' %>; window.syncInProgress = <%= syncInProgress ? 'true' : 'false' %>;