Gil Goldshlager
Gil Goldshlager

Reputation: 771

Synchronizing sync and local chrome.storage

I would like to know how to handle both local and sync storage in the right way in Chrome extension please.

This is my case:
I'm working on an extension for only a specific site (for now),
which contains a content-script and a popup.
The popup contains options where the user can make changes, then the values are sent to the content-script to show the changes on the page.

I'm looking to make as less saving and retrieving storage tasks as possible, and that in the end it will get saved in the sync storage and not just in local.
The sync storage got a per-minute limit, where the local one doesn't.

I know how to listen to the popup closed call from the content-script using a long-lived connection and listen to the onConnect and onDisconnect, and then I can do a save task, but is there a better way to save reading and writing to the storage?
All I can think of was having a background script where I can store the changes in variables and then just send them back and forward to and from the content-script and popup, so it's like having a storage without actually using the storage, but then how can I detect when the user leaves the specific domain and then do the single saving task, and also close/stop the background/event script?

Upvotes: 1

Views: 1434

Answers (1)

Xan
Xan

Reputation: 77591

The current limit on chrome.storage.sync sustained operations is 1 every 2 seconds (more accurately 1800 per hour), and a burst rate limit of 120 per minute.

So, your job is to ensure sync happens no more often than once per 2 seconds.

I would make an event page that deals with chrome.storage.onChanged event and syncs the two areas. Which is a surprisingly hard task due to local echo!

// event.js, goes into background.scripts in manifest

// Those will not persist if event page is unloaded
var timeout;
var queuedChanges = {};
var syncStamp = 1;

chrome.storage.onChanged.addListener(function(changes, area) {
  // Check if it's an echo of our changes
  if(changes._syncStamp && changes._syncStamp.newValue == syncStamp) {
    return;
  }

  if(area == "local") {
    // Change in local storage: queue a flush to sync

    // Reset timeout
    if(timeout) { clearTimeout(timeout); }

    // Merge changes with already queued ones
    for(var key in changes) {
      // Just overwrite old change; we don't care about last newValue
      queuedChanges[key] = changes[key];
    }

    // Schedule flush
    timeout = setTimeout(flushToSync, 3000);

  } else {
    // Change in sync storage: copy to local

    if(changes._syncStamp && changes._syncStamp.newValue) {
      // Ignore those changes when they echo as local
      syncStamp = changes._syncStamp.newValue;
    }
    commitChanges(changes, chrome.storage.local);
  }
});

function flushToSync() {
  // Be mindful of what gets synced: there are also size quotas
  // If needed, filter queuedChanges here

  // Generate a new sync stamp
  // With random instead of sequential, there's a really tiny chance
  //   changes will be ignored, but no chance of stamp overflow
  syncStamp = Math.random();
  queuedChanges._syncStamp = {newValue: syncStamp};

  // Process queue for committing
  commitChanges(queuedChanges, chrome.storage.sync);

  // Reset queue
  queuedChanges = {};
  timeout = undefined;
}

function commitChanges(changes, storage) {
  var setData = {};

  for(var key in changes) {
    setData[key] = changes[key].newValue;
  }

  storage.set(setData, function() {
    if(chrome.runtime.lastError) {
      console.error(chrome.runtime.lastError.message);
    }
  });
}

The idea here is to sync 3 seconds after the last change to local. Each new change is added to the queue and resets the countdown. And while Chrome normally does not honor DOM timers in event pages, 3 seconds is short enough to complete before the page is shut down.

Also, note that updating an area from this code will fire the event again. This is considered a bug (compare with window.onstorage not firing for changes within current document), but meanwhile I added the _syncStamp property. It is used to distinguish the local echo, though there is a tiny chance that the stamp will result in a collision

Your other code (content script) should probably also rely on onChanged event instead of a custom "okay, I changed a value!" message.

Upvotes: 2

Related Questions