JavaScript обработчик событий, работающий с асинхронным c API хранилища данных, вызывающий состояние гонки - PullRequest
2 голосов
/ 11 июля 2020

Мне нужно обновлять некоторые данные каждый раз, когда срабатывает определенное событие браузера (например, когда закрывается вкладка браузера):

chrome.tabs.onRemoved.addListener(async (tabId) => {
  let data = await getData(); // async operation

  ...                         // modify data 

  await setData(data);        // async operation
});

Проблема в том, что когда несколько таких событий запускаются быстро, асинхронный c getData() может вернуть устаревший результат при последующем вызове обработчика событий до того, как setData() получит шанс завершить sh в более ранних, что приведет к несогласованному результату.

Если обработчик событий может выполнить синхронно, то эта проблема не возникнет, но getData() и setData() оба являются асинхронными c операциями.

Это состояние гонки? Какой рекомендуемый шаблон для обработки этого типа logi? c?

--- Обновление ---

Чтобы предоставить больше контекста, getData() и setData() являются просто обещанной версией некоторого Chrome API хранилища:

async function getData() {
  return new Promise(resolve => {
    chrome.storage.local.get(key, function(data) => {
      // callback
    });
  });
}

async function setData() {
  return new Promise(resolve => {
    chrome.storage.local.set({ key: value }, function() => {
      // callback
    });
  });
}

Я заключил вызов API в Promise для удобства чтения, но я думаю, что это асинхронный c op в любом случае?

Ответы [ 2 ]

2 голосов
/ 11 июля 2020

У вас довольно классное c состояние гонки для хранилища данных с асинхронным API, а состояние гонки еще хуже, если вы используете асинхронные операции при обработке данных (между getData() и setData(). Асинхронные операции позволяют другому событию запускаться в середине вашей обработки, разрушая атомарность вашей последовательности событий.

Вот идея, как поместить входящий tabId в очередь и убедиться, что вы только обработка одного из этих событий за раз:

const queue = [];

chrome.tabs.onRemoved.addListener(async (newTabId) => {
    queue.push(newTabId);
    if (queue.length > 1) {
        // already in the middle of processing one of these events
        // just leave the id in the queue, it will get processed later
        return;
    }
    async function run() {
        // we will only ever have one of these "in-flight" at the same time
        try {
            let tabId = queue[0];
            let data = await getData(); // async operation

            ...                         // modify data 

            await setData(data);        // async operation
        } finally {
            queue.shift();              // remove this one from the queue
        }
    }
    while (queue.length) {
        try {
            await run();
        } catch(e) {
            console.log(e);
            // decide what to do if you get an error
        }
    }
});

Это можно было бы сделать более универсальным c, чтобы его можно было многократно использовать в нескольких местах (каждое со своей собственной очередью), например:

function enqueue(fn) {
    const queue = [];
    return async function(...args) {
        queue.push(args);       // add to end of queue
        if (queue.length > 1) {
            // already processing an item in the queue,
            // leave this new one for later
            return;
        }
        async function run() {
            try {
                const nextArgs = queue[0];  // get oldest item from the queue
                await fn(...nextArgs);      // process this queued item
            } finally {
                queue.shift();              // remove the one we just processed from the queue
            }
        }
        // process all items in the queue
        while (queue.length) {
            try {
                await run();
            } catch(e) {
                console.log(e);
                // decide what to do if you get an error
            }
        }
    }
}

chrome.tabs.onRemoved.addListener(enqueue(async function(tabId) {
    let data = await getData(); // async operation

    ...                         // modify data

    await setData(data);        // async operation
}));
0 голосов
/ 11 июля 2020

JS ascync / await на самом деле не превращает JS код в синхронный.

Все, что вам нужно сделать, это заблокировать событие l oop в getData с помощью Promisse.all.

Итак,

chrome.tabs.onRemoved.addListener(async (tabId) => {
   ...                         // turns in a composition 

  await setData(Promise.all([getData])[0]); // async composition
});

Вы можете выполнить асинхронную c композицию с блоком на событии l oop, когда событие запускается, виртуальная машина будет иметь список с событиями и блок на await getData.

Фактически не существует асинхронной c композиции, это всего лишь трюк с виртуальной машиной для блокировки события l oop и ожидания результата операции, заставляющей виртуальную машину обрабатывать это как список и списки не ждут.

Будьте осторожны с вашим кодом, чтобы его можно было прочитать при использовании композиций.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...