Search code examples
javascriptloopsasynchronousgoogle-chrome-extensionpromise

Create async instances of loops and manage them


I am using a chrome extension to activate infinite async instances of loops so they do not conflict with each other.

There is a list of values and one item is being passed to each individual loop. Those loops are executed in the content.js and are being managed by the background.js but they are initialized, started and cancelled from the popup.js.

Now big Question is how do I use best practices to make the management of multiple async loops as easy as possible? Is there any possible way to also cancel these loops in an easy way?

example code:

content.js:

chrome.runtime.onMessage.addListener(
     function(request, sender, sendResponse) {
if(request.message.active){
      console.log("do something");
      dispatch(request.message);
}});


async function dispatch(alternator) {
    if (alternator.active) {
        await new Promise(resolve => setTimeout(resolve, alternator.timeout));
        console.log("do something");
    }
    return;
}

This background.js should have a list of async loops to manage in an array or something easy to manage. The for-loop is consuming too much time and the timeout is causing too much load.

background.js

async function dispatchBackground() {
    while (1) {
        for (let i = 0; i < alternator.length; i++) {
            if(alternator[i].active){
                chrome.tabs.sendMessage(alternator[i].tab_id, {"message": alternator[i]});
            }
        }
        await new Promise(resolve => setTimeout(resolve, 100));
    }
    return;
}

Solution

  • You should probably use a library.

    ...but that would be boring!

    In the following 👇 code, macrotask uses requestIdleCallback to run a callback on a JS runtime macrotask.

    The user supplies the data to process, the logic to run (synchronously) at each step, and the continuation condition; they do not have to worry about explicitly yielding to an API.

    Function createTask constructs a generator to return steps until the continuationPredicate returns false. Generator functions enable us to suspend and resume synchronous code - which we need to do here to switch between tasks in a round-robin fashion. A more advanced solution could prioritise tasks according to a heuristic.

    createCircularList returns a wrapper around an array that exposes add, remove, and next (get the next item in creation order or, if we are at the "end", loop around to the first item again).

    createScheduler maintains the task list. While there are tasks remaining in the task list, this function will identify the next task, schedule its next step on a macrotask, and wait for that step to complete. If that was the final step in the current task, the task is then removed from the task list.

    Note that the precise interleaving of the output of this code will depend on things like how busy your machine is. The intent of the demonstration is to show how the task queue can be added-to while it is being drained.

    const log = console.log
    const nop = () => void 0
    const stepPerItem = (_, i, data) => i < data.length
    const macrotask = (cb) => (...args) => new Promise((res) => (typeof requestIdleCallback ? requestIdleCallback : setTimeout)(() => res(cb(...args))))
    
    const createTask = (data, 
                        step, 
                        continuePredicate = stepPerItem, 
                        acc = null, 
                        onDone = nop) => 
        (function*(i = 0) {
            while(continuePredicate(acc, i, data)) {
                acc = step(acc, i, data)
                yield [acc, onDone]
                i++
            }
            return [acc, onDone]
        })()
    
    const createCircularList = (list = []) => {
        const add = list.push.bind(list)
        const remove = (t) => list.splice(list.indexOf(t), 1)
        
        const nextIndex = (curr, currIndex = list.indexOf(curr)) =>
            (currIndex === list.length - 1) ? 0 : currIndex + 1
        
        const next = (curr) =>
            list.length ? list[nextIndex(curr)] : null
        
        return { add, remove, next }
    }
    
    const createScheduler = (tasks = createCircularList()) => {    
        let isRunning = false
        
        const add = (...tasksToAdd) =>        
            (tasksToAdd.forEach((t) => tasks.add(t)), 
             !isRunning && (isRunning = true, go()))
        
        const remove = tasks.remove.bind(tasks)
        
        const go = async (t = null) => {
            while(t = tasks.next(t))
                await macrotask(({ done, value: [result, onDone] } = t.next()) =>
                    done && (tasks.remove(t), onDone(result)))()            
            isRunning = false
        }
        
        return { add, remove }
    }
    
    const scheduler = createScheduler()
    const task1 = createTask([...Array(5)], (_, i) => log('task1', i))
    const task2 = createTask([...Array(5)], (_, i) => log('task2', i))
    const task3 = createTask([...Array(5)], (_, i) => log('task3', i))
    
    scheduler.add(task1, task2)
    setTimeout(() => scheduler.add(task3), 50) // you may need to fiddle with the `setTimeout` delay here to observe meaningful interleaving