1

Summary:

I've built a chrome extension that reaches out to external API to fetch some data. Sometimes that data returns quickly, sometimes it takes 4 seconds or so. I'm often doing about 5-10 in rapid succession (this is a scraping tool).

Previously, a lot of requests were dropped because the service worker in V3 of Manifest randomly shuts down. I thought I had resolved that. Then I realized there was a race condition because local storage doesn't have a proper queue.

Current Error - Even with all these fixes, requests are still being dropped. The external API returns the correct data successfully, but it seems like the extension never gets it. Hoping someone can point me in the right direction.

Relevant code attached, I imagine it will help someone dealing with these queue and service worker issues.

Local Storage queue

let writing: Map<string, Promise<any>> = new Map();

let updateUnsynchronized = async (ks: string[], f: Function) => {
  let m = await new Promise((resolve, reject) => {
    chrome.storage.local.get(ks, res => {
      let m = {};
      for (let k of ks) {
        m[k] = res[k];
      }
      maybeResolveLocalStorage(resolve, reject, m);
    });
  });
  // Guaranteed to have not changed in the meantime
  let updated = await new Promise((resolve, reject) => {
    let updateMap = f(m);
    chrome.storage.local.set(updateMap, () => {
      maybeResolveLocalStorage(resolve, reject, updateMap);
    });
  });
  console.log(ks, 'Updated', updated);
  return updated;
};

export async function update(ks: string[], f: Function) {
  let ret = null;

  // Global lock for now
  await navigator.locks.request('global-storage-lock', async lock => {
    ret = await updateUnsynchronized(ks, f);
  });

  return ret;
}

Here's the main function

export async function appendStoredScrapes(
  scrape: any,
  fromHTTPResponse: boolean
) {
  let updated = await update(['urlType', 'scrapes'], storage => {
    const urlType = storage.urlType;
    const scrapes = storage.scrapes;
    const {url} = scrape;

    if (fromHTTPResponse) {
      // We want to make sure that the url type at time of scrape, not time of return, is used
      scrapes[url] = {...scrapes[url], ...scrape};
    } else {
      scrapes[url] = {...scrapes[url], ...scrape, urlType};
    }

    return {scrapes};
  });

  chrome.action.setBadgeText({text: `${Object.keys(updated['scrapes']).length}`});
}

Keeping the service worker alive

let defaultKeepAliveInterval = 20000;

// To avoid GC
let channel;

// To be run in content scripts
export function contentKeepAlive(name : string) {
  channel = chrome.runtime.connect({ name });

  channel.onDisconnect.addListener(() => contentKeepAlive(name));

  channel.onMessage.addListener(msg => { });
}

let deleteTimer = (chan : any) => {
  if (chan._timer) {
    clearTimeout(chan._timer);
    delete chan._timer;
  }
}

let backgroundForceReconnect = (chan : chrome.runtime.Port) => {
  deleteTimer(chan);
  chan.disconnect();
}

// To be run in background scripts
export function backgroundKeepAlive(name : string) {
  chrome.runtime.onConnect.addListener(chan => {
    if (chan.name === name) {
      channel = chan;
      channel.onMessage.addListener((msg, chan) => { });
      channel.onDisconnect.addListener(deleteTimer);
      channel._timer = setTimeout(backgroundForceReconnect, defaultKeepAliveInterval, channel);
    }
  });
}

// "Always call sendResponse() in your chrome.runtime.onMessage listener even if you don't need
// the response. This is a bug in MV3." — https://stackoverflow.com/questions/66618136/persistent-service-worker-in-chrome-extension
export function defaultSendResponse (sendResponse : Function) {
  sendResponse({ farewell: 'goodbye' });
}

Relevant parts of background.ts

backgroundKeepAlive('extension-background');

let listen = async (request, sender, sendResponse) => {
  try {
    if (request.message === 'SEND_URL_DETAIL') {
      const {url, website, urlType} = request;
      await appendStoredScrapes({url}, false);
      let data = await fetchPageData(url, website, urlType);
      console.log(data, url, 'fetch data returned background');
      await appendStoredScrapes(data, true);
      defaultSendResponse(sendResponse);
    } else if (request.message === 'KEEPALIVE') {
      sendResponse({isAlive: true});
    } else {
      defaultSendResponse(sendResponse);
    }
  } catch (e) {
    console.error('background listener error', e);
  }
};

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  listen(request, sender, sendResponse);
});
dizzy
  • 1,177
  • 2
  • 12
  • 34
  • To invoke sendResponse asynchronously (SEND_URL_DETAIL case) you need to add `return true` at the end of the main onMessage listener. – wOxxOm Jun 17 '22 at 04:10
  • @wOxxOm you're always so helpful! Thank you for catching that. – dizzy Jun 17 '22 at 18:33

0 Answers0