Реализация синхронизации данных между устройствами в браузерном расширении
Синхронизация в браузерных расширениях строится на двух механизмах: chrome.storage.sync для простых настроек и собственный бэкенд для произвольных данных. Первый вариант бесплатен, но ограничен. Второй требует авторизации и инфраструктуры.
chrome.storage.sync: ограничения и реальные возможности
Лимиты хранилища sync (Chrome):
- Общий объём: 102 400 байт
- Максимальный размер одного значения: 8 192 байт
- Максимальное число ключей: 512
- Максимальное число операций записи в минуту: 1 800 (суммарно), 120 на ключ
Firefox поддерживает browser.storage.sync с похожими ограничениями через Firefox Sync.
Для настроек расширения этого достаточно. Для пользовательского контента (закладки, заметки, история) — нет.
// Утилита для работы с sync c учётом лимита на значение
async function syncSet(key, value) {
const serialized = JSON.stringify(value);
if (new Blob([serialized]).size > 8000) {
// Разбиваем на чанки по 7КБ
const chunks = chunkString(serialized, 7000);
const batch = {};
batch[`${key}__count`] = chunks.length;
chunks.forEach((chunk, i) => {
batch[`${key}__${i}`] = chunk;
});
await chrome.storage.sync.set(batch);
} else {
await chrome.storage.sync.set({ [key]: value });
}
}
async function syncGet(key) {
const result = await chrome.storage.sync.get([key, `${key}__count`]);
if (result[`${key}__count`]) {
const count = result[`${key}__count`];
const chunkKeys = Array.from({ length: count }, (_, i) => `${key}__${i}`);
const chunks = await chrome.storage.sync.get(chunkKeys);
const serialized = chunkKeys.map(k => chunks[k] ?? '').join('');
return JSON.parse(serialized);
}
return result[key] ?? null;
}
function chunkString(str, size) {
const chunks = [];
for (let i = 0; i < str.length; i += size) {
chunks.push(str.slice(i, i + size));
}
return chunks;
}
Конфликты при синхронизации
Пользователь мог изменить настройки на двух устройствах офлайн. При возврате онлайн оба устройства запишут свою версию. chrome.storage.sync не имеет механизма merge — побеждает последняя запись.
Для более предсказуемого поведения используйте версионированные объекты:
async function mergeSettings(incoming) {
const local = await chrome.storage.sync.get('settings');
const current = local.settings ?? { version: 0, data: defaultSettings };
if (incoming.version <= current.version) {
// Входящие данные старее — игнорируем
return current;
}
// Простая стратегия: побеждает более новая версия
await chrome.storage.sync.set({ settings: incoming });
return incoming;
}
Собственный бэкенд для полноценной синхронизации
Когда нужна синхронизация произвольного объёма данных, реализуют сервер с identity + storage. Типичная схема:
- Пользователь авторизуется через OAuth (Google, GitHub)
- Расширение хранит access token в
chrome.storage.local - При изменениях данных — push на сервер с меткой времени
- При старте/фокусе браузера — pull изменений с сервера
// sync/client.js
const API_BASE = 'https://api.your-extension.com/v1';
async function getAuthToken() {
const { authToken, tokenExpiry } = await chrome.storage.local.get(['authToken', 'tokenExpiry']);
if (!authToken || Date.now() > tokenExpiry) {
return null;
}
return authToken;
}
async function pushChanges(changes) {
const token = await getAuthToken();
if (!token) return;
const { lastSyncedAt = 0 } = await chrome.storage.local.get('lastSyncedAt');
const response = await fetch(`${API_BASE}/sync/push`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
changes,
clientId: await getClientId(),
timestamp: Date.now()
})
});
if (!response.ok) throw new Error(`Push failed: ${response.status}`);
const { serverTimestamp } = await response.json();
await chrome.storage.local.set({ lastSyncedAt: serverTimestamp });
}
async function pullChanges() {
const token = await getAuthToken();
if (!token) return;
const { lastSyncedAt = 0 } = await chrome.storage.local.get('lastSyncedAt');
const response = await fetch(
`${API_BASE}/sync/pull?since=${lastSyncedAt}&clientId=${await getClientId()}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
if (!response.ok) return;
const { changes, serverTimestamp } = await response.json();
if (changes.length > 0) {
await applyRemoteChanges(changes);
await chrome.storage.local.set({ lastSyncedAt: serverTimestamp });
// Уведомить все открытые вкладки расширения
const views = chrome.extension.getViews();
views.forEach(view => {
view.postMessage?.({ type: 'SYNC_COMPLETE', changes });
});
}
}
async function getClientId() {
let { clientId } = await chrome.storage.local.get('clientId');
if (!clientId) {
clientId = crypto.randomUUID();
await chrome.storage.local.set({ clientId });
}
return clientId;
}
Авторизация через chrome.identity
Chrome предоставляет встроенный OAuth flow через chrome.identity.getAuthToken:
async function signInWithGoogle() {
return new Promise((resolve, reject) => {
chrome.identity.getAuthToken({ interactive: true }, async (token) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
// Получаем информацию о пользователе
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${token}` }
});
const userInfo = await response.json();
// Обмениваем Google token на токен своего API
const authResponse = await fetch(`${API_BASE}/auth/google`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ googleToken: token })
});
const { accessToken, expiresAt } = await authResponse.json();
await chrome.storage.local.set({
authToken: accessToken,
tokenExpiry: expiresAt,
userInfo
});
resolve(userInfo);
});
});
}
Для Firefox используйте browser.identity.launchWebAuthFlow с аналогичной логикой.
Синхронизация в реальном времени
Если нужна синхронизация без задержки (например, совместное использование расширения на нескольких мониторах), используйте WebSocket или SSE в service worker:
// background/sw.js — SSE подключение
let eventSource = null;
async function startRealTimeSync() {
const token = await getAuthToken();
if (!token || eventSource) return;
// SSE через fetch + ReadableStream (работает в SW, EventSource — нет в SW)
const response = await fetch(`${API_BASE}/sync/stream`, {
headers: { Authorization: `Bearer ${token}` }
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n').filter(l => l.startsWith('data: '));
for (const line of lines) {
try {
const event = JSON.parse(line.slice(6));
await applyRemoteChanges([event]);
} catch {}
}
}
}
Типичный сценарий внедрения: начинают с chrome.storage.sync для MVP, добавляют собственный бэкенд по мере роста требований к объёму и надёжности.







