Skip to content

Commit

Permalink
fix(sync): move options sync to background process
Browse files Browse the repository at this point in the history
  • Loading branch information
smalluban committed Nov 18, 2024
1 parent 28f5a91 commit c09e263
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 172 deletions.
1 change: 1 addition & 0 deletions src/background/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import './serp.js';

import './helpers.js';
import './external.js';
import './sync.js';

import './reporting/index.js';
import './telemetry/index.js';
Expand Down
61 changes: 1 addition & 60 deletions src/background/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0
*/
import { store } from 'hybrids';

import Options, { sync } from '/store/options.js';
import Session, { UPDATE_SESSION_ACTION_NAME } from '/store/session.js';

import { UPDATE_SESSION_ACTION_NAME } from '/store/session.js';
import { HOME_PAGE_URL, ACCOUNT_PAGE_URL } from '/utils/api.js';

// Trigger options sync every one day
const ALARM_SYNC_OPTIONS = 'session:sync:options';
const ALARM_SYNC_OPTIONS_RATE = 1 * 60 * 24; // 1 day in minutes

// Try to check cookies every 30 days if the user is still logged in
const ALARM_UPDATE_SESSION = 'session:update';
const ALARM_UPDATE_SESSION_DELAY = 1000 * 60 * 24 * 30; // 30 days in milliseconds

async function syncOptions() {
sync(await store.resolve(Options));
}

// Observe cookie changes (login/logout actions)
chrome.webNavigation.onDOMContentLoaded.addListener(async ({ url = '' }) => {
if (url === HOME_PAGE_URL || url.includes(ACCOUNT_PAGE_URL)) {
const { user } = await store.resolve(Session);

if (user) {
if (!(await chrome.alarms.get(ALARM_SYNC_OPTIONS))) {
chrome.alarms.create(ALARM_SYNC_OPTIONS, {
periodInMinutes: ALARM_SYNC_OPTIONS_RATE,
});
}

if (!(await chrome.alarms.get(ALARM_UPDATE_SESSION))) {
chrome.alarms.create(ALARM_UPDATE_SESSION, {
when: Date.now() + ALARM_UPDATE_SESSION_DELAY,
});
}
} else {
chrome.alarms.clear(ALARM_SYNC_OPTIONS);
chrome.alarms.clear(ALARM_UPDATE_SESSION);
}

syncOptions();

// Send message to update session in other contexts
chrome.runtime
.sendMessage({ action: UPDATE_SESSION_ACTION_NAME })
Expand All @@ -60,25 +23,3 @@ chrome.webNavigation.onDOMContentLoaded.addListener(async ({ url = '' }) => {
.catch(() => null);
}
});

chrome.alarms.onAlarm.addListener(async ({ name }) => {
switch (name) {
case ALARM_SYNC_OPTIONS:
syncOptions();
break;
case ALARM_UPDATE_SESSION: {
const { user } = await store.resolve(Session);

if (!user) {
chrome.alarms.clear(ALARM_UPDATE_SESSION);
} else {
chrome.alarms.create(ALARM_UPDATE_SESSION, {
when: Date.now() + ALARM_UPDATE_SESSION_DELAY,
});
}
break;
}
default:
break;
}
});
144 changes: 144 additions & 0 deletions src/background/sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Ghostery Browser Extension
* https://www.ghostery.com/
*
* Copyright 2017-present Ghostery GmbH. All rights reserved.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0
*/
import { store } from 'hybrids';

import Options, { SYNC_OPTIONS } from '/store/options.js';
import Session from '/store/session.js';
import { getUserOptions, setUserOptions } from '/utils/api.js';
import * as OptionsObserver from '/utils/options-observer.js';
import { HOME_PAGE_URL, ACCOUNT_PAGE_URL } from '/utils/api.js';

async function sync(options, prevOptions) {
if (sync.pending) {
console.warn('[sync] Sync already in progress...');
return;
}

try {
sync.pending = true;

// Do not sync if revision is set or terms and sync options are false
if (!options.terms || !options.sync) {
return;
}

const { user } = await store.resolve(Session);

// If user is not logged in, clean up options revision and return
if (!user) {
if (options.revision !== 0) {
store.set(Options, { revision: 0 });
}
return;
}

const keys =
prevOptions &&
SYNC_OPTIONS.filter(
(key) => !OptionsObserver.isOptionEqual(options[key], prevOptions[key]),
);

// If options update, set revision to "dirty" state
if (keys && options.revision > 0) {
// Updated keys are not synchronized
if (keys.length === 0) return;

options = await store.set(Options, { revision: options.revision * -1 });
}

const serverOptions = await getUserOptions();

// Server has newer options - merge with local options
// The try/catch block is used to prevent failure of updating local options
// with server options with obsolete structure
try {
if (serverOptions.revision > Math.abs(options.revision)) {
console.info(
'[sync] Merging server options with revision:',
serverOptions.revision,
);
const values = SYNC_OPTIONS.reduce(
(acc, key) => {
if (
!keys?.includes(key) &&
hasOwnProperty.call(serverOptions, key)
) {
acc[key] = serverOptions[key];
}

return acc;
},
{ revision: serverOptions.revision },
);

options = await store.set(Options, values);
}
} catch (e) {
console.error(`[sync] Error while merging server options: `, e);
}

// Set options or update:
// * No revision on server - initial sync
// * Keys are passed - options update
// * Revision is negative - local options are dirty (not synced)
if (!serverOptions.revision || keys || options.revision < 0) {
console.info('[sync] Syncing options with updated keys:', keys);
const { revision } = await setUserOptions(
SYNC_OPTIONS.reduce(
(acc, key) => {
if (hasOwnProperty.call(options, key)) {
acc[key] = options[key];
}
return acc;
},
{ revision: serverOptions.revision + 1 },
),
);

// Update local revision
await store.set(Options, { revision });
console.info('[sync] Options synced with revision:', revision);
}
} catch (e) {
console.error(`[sync] Error while syncing options: `, e);
} finally {
sync.pending = false;
}
}

// Sync options on startup and when options change
OptionsObserver.addListener(function syncOptions(options, prevOptions) {
// Sync options on startup
if (!prevOptions) {
sync(options);
}

// Sync options when options change (skip on revision change)
else if (options.revision === prevOptions.revision) {
sync(options, prevOptions);
}
});

// Sync options when a user logs in/out directly
// from the ghostery.com page (not from the settings page)
chrome.webNavigation.onDOMContentLoaded.addListener(async ({ url = '' }) => {
if (url === HOME_PAGE_URL || url.includes(ACCOUNT_PAGE_URL)) {
store.resolve(Options).then((options) => sync(options));
}
});

// Sync options on demand - triggered by the options page and panel
// to force sync options when opened
chrome.runtime.onMessage.addListener((msg) => {
if (msg.action === 'syncOptions') {
store.resolve(Options).then((options) => sync(options));
}
});
11 changes: 6 additions & 5 deletions src/pages/panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ mount(document.body, {
`,
});

/* Ping telemetry on panel open */
// Ping telemetry on panel open
chrome.runtime.sendMessage({ action: 'telemetry', event: 'engaged' });

/*
Safari extension popup has a bug, which focuses visibly the first element on the page
when the popup is opened. This is a workaround to remove the focus.
*/
// Sync options with background
chrome.runtime.sendMessage({ action: 'syncOptions' });

// Safari extension popup has a bug, which focuses visibly the first element on the page
// when the popup is opened. This is a workaround to remove the focus.
if (__PLATFORM__ === 'safari') {
window.addEventListener('load', () => {
setTimeout(() => {
Expand Down
3 changes: 3 additions & 0 deletions src/pages/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ store
};
}

// Sync options with background
chrome.runtime.sendMessage({ action: 'syncOptions' });

mount(document.body, Settings);
})
.catch(() => {
Expand Down
52 changes: 25 additions & 27 deletions src/pages/settings/views/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ function openGhosteryPage(url) {
details.tabId === tab.id &&
details.url.startsWith(ACCOUNT_PAGE_URL)
) {
chrome.tabs.remove(tab.id);
chrome.webNavigation.onCommitted.removeListener(onSuccess);
chrome.tabs.remove(tab.id);
}
};

Expand All @@ -36,7 +36,12 @@ function openGhosteryPage(url) {
chrome.webNavigation.onCommitted.removeListener(onSuccess);
chrome.tabs.onRemoved.removeListener(onRemove);

// The tab is closed before the background listeners can catch the event
// so we need to refresh session and trigger sync manually
store.clear(Session);
chrome.runtime.sendMessage({ action: 'syncOptions' });

// Restore the original tab
chrome.tabs.update(currentTab[0].id, { active: true });
}
};
Expand Down Expand Up @@ -120,34 +125,27 @@ export default {
</div>
`)}
</settings-card>
${store.ready(session) &&
html`
<div
layout="grid"
style="${{ opacity: !session.user ? 0.5 : undefined }}"
<div layout="grid">
<ui-toggle
value="${options.sync}"
onchange="${html.set(options, 'sync')}"
>
<ui-toggle
disabled="${!session.user}"
value="${session.user && options.sync}"
onchange="${session.user && html.set(options, 'sync')}"
>
<div layout="column grow gap:0.5">
<div layout="row gap items:center">
<ui-icon
name="websites"
color="gray-600"
layout="size:2"
></ui-icon>
<ui-text type="headline-xs">Settings Sync</ui-text>
</div>
<ui-text type="body-m" mobile-type="body-s" color="gray-600">
Saves and synchronizes your custom settings between browsers
and devices.
</ui-text>
<div layout="column grow gap:0.5">
<div layout="row gap items:center">
<ui-icon
name="websites"
color="gray-600"
layout="size:2"
></ui-icon>
<ui-text type="headline-xs">Settings Sync</ui-text>
</div>
</ui-toggle>
</div>
`}
<ui-text type="body-m" mobile-type="body-s" color="gray-600">
Saves and synchronizes your custom settings between browsers
and devices.
</ui-text>
</div>
</ui-toggle>
</div>
</section>
</settings-page-layout>
</template>
Expand Down
Loading

0 comments on commit c09e263

Please sign in to comment.