Coder Social home page Coder Social logo

ealmansi / set-interval-async Goto Github PK

View Code? Open in Web Editor NEW
105.0 4.0 14.0 1.43 MB

Modern version of setInterval for promises and async functions.

Home Page: https://github.com/ealmansi/set-interval-async

License: MIT License

JavaScript 5.56% TypeScript 94.44%
setinterval clearinterval cleartimeout concurrency promises recurrent settimeout async-await es6 javascript

set-interval-async's Introduction

setIntervalAsync
License: MIT npm version Build Status Coverage Status PRs Welcome

Modern version of setInterval for promises and async functions available in Node.js and browsers.

setIntervalAsync works both on Node.js and in the browser, providing the same
familiar interface as setInterval for asynchronous functions, while preventing
multiple executions from overlapping in time.

Getting Started

Node.js

First, install setIntervalAsync using npm or yarn:

# Using npm:
npm install set-interval-async

# Or using yarn:
yarn add set-interval-async

Now, you can require setIntervalAsync in CommonJS:

const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async');

Or else, you can use ES6 modules syntax:

import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async';

Browser

In the browser, you can add a script tag in your HTML:

<!-- Load from unpkg.com -->
<script src="https://unpkg.com/set-interval-async"></script>

<!-- Or from jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/set-interval-async"></script>

After the script is loaded, a variable called SetIntervalAsync will be defined in the global context. From there you can retrieve the setIntervalAsync and clearIntervalAsync functions.

var setIntervalAsync = SetIntervalAsync.setIntervalAsync;
var clearIntervalAsync = SetIntervalAsync.clearIntervalAsync;

Usage

In the most basic scenario, you can use setIntervalAsync the same way you would use vanilla setInterval. For example, the following code will print 'Hello' to the console once every second.

const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async');

setIntervalAsync(() => {
  console.log('Hello')
}, 1000);

However, you can also provide an async function (or a function returning a promise), which has the added nicety that now you can wait until the cycle is fully stopped before moving on by using clearIntervalAsync.

const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async');

const timer = setIntervalAsync(async () => {
  console.log('Hello')
  await doSomeWork()
  console.log('Bye')
}, 1000);

// Or equivalently:

const timer = setIntervalAsync(() => {
  console.log('Hello')
  return doSomeWork().then(
    () => console.log('Bye')
  )
}, 1000);


// Later:

await clearIntervalAsync(timer);

// At this point, all timers have been cleared, and the last
// execution is guaranteed to have finished as well.

This is particularly useful when - for example at the end of a unit test - you want to make sure that no asynchronous code continues running by the time your test manager moves on to the next one.

it('should test something', async () => {
  const timer = setIntervalAsync(/* ... */);
  
  // Do some assertions.
  
  await clearIntervalAsync(timer);
  // Interval is now fully stopped.
});

When Should I Use setIntervalAsync?

Where setIntervalAsync really shines is in those situations where the given asynchronous function might take longer to compute than the configured interval and, at the same time, is not safe to execute more than once at a time. Using vanilla setInterval will break your code in this scenario, whereas setIntervalAsync guarantees that the function will never execute more than once at the same time.

For example, consider the following code:

async function processQueue (queue) {
  if (queue.length === 0) {
    return;
  }
  let head = queue[0];
  await doSomeWork(head);
  queue.shift(); // Removes the first element.
}

The function above should never get called again before the previous execution is completed. Otherwise, the queue's first element will get processed twice, and the second element will be skipped.

However, with setIntervalAsync, the following code is perfectly safe:

setIntervalAsync(processQueue, 1000, queue)

since setIntervalAsync will guarantee that the function is never executed more than once at any given moment.

You can choose whether you wish to use the Dynamic or Fixed strategies, which will either launch every execution as soon as possible or set a fixed delay between the end of one execution and the start of the next one. See Dynamic and Fixed setIntervalAsync for more details.

Dynamic and Fixed setIntervalAsync

setIntervalAsync provides two strategies which can be used to prevent a recurring function from executing more than once at any given moment:

  • Dynamic: If possible, the given function is called once every interval milliseconds. If any execution takes longer than the desired interval, the next execution is delayed until the previous one has finished, and called immediately after this condition is reached.

    Dynamic setIntervalAsync diagram.

  • Fixed: The given function is called repeatedly, guaranteeing a fixed delay of interval milliseconds between the end of one execution and the start of the following one.

    Fixed setIntervalAsync diagram.

You can choose whichever strategy works best for your application. When in doubt, the Dynamic strategy will likely suffice for most use cases, keeping the interval as close as possible to the desired one. The default strategy is Dynamic.

In Node.js

You can require a specific strategy for setIntervalAsync using CommonJS with the following snippets:

// Dynamic strategy.
const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async/dynamic');

// Fixed strategy.
const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async/fixed');

Or else, you can use ES6 modules syntax:

// Dynamic strategy.
import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async/dynamic';

// Fixed strategy.
import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async/fixed';

In the Browser

After the library has been loaded to the page, a variable called SetIntervalAsync will be defined in the global context. From there you can retrieve the setIntervalAsync from your desired strategy and clearIntervalAsync functions.

// Dynamic strategy.
var setIntervalAsync = SetIntervalAsync.dynamic.setIntervalAsync;
var clearIntervalAsync = SetIntervalAsync.clearIntervalAsync;

// Fixed strategy.
var setIntervalAsync = SetIntervalAsync.fixed.setIntervalAsync;
var clearIntervalAsync = SetIntervalAsync.clearIntervalAsync;

API

Function setIntervalAsync

Executes the given handler every intervalMs milliseconds, while preventing multiple concurrent executions. The handler will never be executed concurrently more than once in any given moment.

See Dynamic and Fixed setIntervalAsync for more details on which strategies can be used to determine the effective interval between executions when the handler takes longer than the target interval to complete. The default strategy is Dynamic.

function setIntervalAsync<HandlerArgs extends unknown[]>(
  handler: SetIntervalAsyncHandler<HandlerArgs>,
  intervalMs: number,
  ...handlerArgs: HandlerArgs
): SetIntervalAsyncTimer<HandlerArgs>;

Note: when intervalMs is less than 1, it will be set to 1. When intervalMs is greater than 2147483647, it will be set to 2147483647. Non-integer values are truncated to an integer.

Function clearIntervalAsync

Stops an execution cycle started by setIntervalAsync. Any ongoing function executions will run until completion, but all future ones will be cancelled.

async function clearIntervalAsync<HandlerArgs extends unknown[]>(
  timer: SetIntervalAsyncTimer<HandlerArgs>
): Promise<void>;

The promise returned will resolve once the timer has been stopped and the ongoing execution (if available) has been completed. If the last execution ends in a promise rejection, the promise returned by clearIntervalAsync will reject with the same value.

Type SetIntervalAsyncHandler

Synchronous or asynchronous function that can be passed in as a handler to setIntervalAsync.

type SetIntervalAsyncHandler<HandlerArgs extends unknown[]> = (
  ...handlerArgs: HandlerArgs
) => void | Promise<void>;

Type SetIntervalAsyncTimer

Return type of setIntervalAsync. Does not have any observable properties, but must be passed in as an argument to clearIntervalAsync to stop execution.

type SetIntervalAsyncTimer<HandlerArgs extends unknown[]>;

Avoiding Deadlock When Clearing an Interval

While calling clearIntervalAsync to stop an interval is perfectly safe in any circumstance, please note that awaiting its result within the async handler itself will lead to undesirable results.

For example, the code below leads to a cyclical promise chain that will never be resolved (the console.log statement is unreachable).

const timer = setIntervalAsync(async () => {
  // ...
  if (shouldStop) {
    await clearIntervalAsync(timer);
    console.log('Stopped!');
  }
}, interval);

This is the case because:

  • await clearIntervalAsync(timer) will not resolve until the last execution has finished, and
  • the last execution will not finish until await clearIntervalAsync(timer) has been resolved.

To prevent this cycle, always allow the async handler to complete without awaiting for the interval to be cleared. For example, by removing the await keyword entirely or by using an immediately-invoked function expression:

  if (shouldStop) {
    (async () => {
      await clearIntervalAsync(timer);
      console.log('Stopped!');
    })();
  }

Motivation

If you've ever had to deal with weird, subtle bugs as a consequence of using setInterval[1] on asynchronous functions, or had to manually reimplement setInterval using setTimeout[2] to prevent multiple executions of the same asynchronous function from overlapping, then this library is a drop-in replacement that will solve your issues.

setInterval runs a given function repeateadly, once every fixed number of milliseconds. This may cause problems whenever the function takes longer to execute than the given interval, since it will be called again before the first execution has finished. This is often a problem for non-reentrant functions; ie. functions that are not designed to allow multiple executions at the same time.

setIntervalAsync is a drop-in replacement of setInterval which shares the same API but is safe to use with non-reentrant, asynchronous functions.

[1] https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
[2] https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval

Contributing

In order to contribute to this project, you will need to first clone the repository:

git clone https://github.com/ealmansi/set-interval-async.git

Make sure that Yarn is installed globally on your system, install all project dependencies, and build the project:

yarn
yarn build

Now, you can run the tests and make sure that everything is up and running correctly:

yarn test

If the previous step succeeds, you're ready to start developing on this project.
Pull requests are welcome!

You can verify that your code follows the project's style conventions the with the following command:

yarn lint

License

MIT

set-interval-async's People

Contributors

dependabot[bot] avatar ealmansi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

set-interval-async's Issues

Add a way to change the interval

Working with async iterators, I am finding myself often needing to change interval settings during the life time of the event, i.e. I would like to be able to do this:

const interval = setIntervalAsync(async () => {
  // After the initial 1000 delay, now the interval is set to 100.
  interval.setInterval(100);
}, 1000);

Example use case: Think of a job queue that needs to back-off when there are no tasks, and increase the rate at which it checks for new tasks when there are tasks.

clearIntervalAsync inside setIntervalAsync callback stops the execution

It seems that calling clearIntervalAsync from a setIntervalAsync callback will successfully stop the interval, but also stop the execution of the callback.

It may be related to #20

example.js:

const { clearIntervalAsync } = require('set-interval-async');
const { setIntervalAsync } = require('set-interval-async/dynamic');

let n = 0;
const timer = setIntervalAsync(async () => {
  if (n === 3) {
    await clearIntervalAsync(timer);
    console.log('this statement will not be executed');
  } else {
    console.log('tick');
    n += 1;
  }
}, 100);
$ npm i set-interval-async
$ node example.js
tick
tick
tick

The console.log after the clearIntervalAsync is never executed (or any other kind of code).

Handling computer sleep

The browser's setInterval is oblivious to a computer sleeping. So if you have a 30min setInterval used to keep a token from expiring, the computer can wakeup, and think it still has 10 more minutes, even though hours have passed.

I was hoping this package would handle something like this. Feel free to close if you're not interested. This is the solution I added, that could be adapted here:

const sleepAdjustedSetInterval = (handler, timeout, ...args) => {
  let baseTime = Date.now();
  const callHandler = () => {
    if (Date.now() - baseTime > timeout) {
      baseTime = Date.now();
      handler();
    }
  };
  return window.setInterval(callHandler, 1000, ...args);
};

Enough stability for production use

Hi, this project interests me.

Compared to andyfleming/interval-promise (github), I prefer set-interval-async bit more as it provides two strategies, "fixed" and "dynamic", although interval-promise (npm) has much higher download frequency (1613 weekly downloads at the time of writing) .

However, set-interval-async is on VERY early stage, so it is quite hesitating to use on production.

Well, though generally it's highly recommended not to use too new modules, but I am not too strict to such a philosophy if the project is enough simple and tested thoroughly.

Before making decision, I'd like to ask weather you think this project is tested well and can be used and promised to be updated (in future) in stable way even for production environment.

P.S. Why do you recommend using exact version(yarn add -E)? Is it to prevent broken update even if you made a mistake that breaks SemVer?

404 error

Hello all. I just tried to bring set-interval-async into the browser and I'm getting the following error in the chrome console:

"DevTools failed to load SourceMap: Could not load content for https://unpkg.com/set-interval-async.iife.js.map: HTTP error: status code 404, net::ERR_HTTP_RESPONSE_CODE_FAILURE"

I put this line from the documentation into my :

<script src="https://unpkg.com/set-interval-async"></script>

I've only used set-interval-async before in node. Anyone know what is going on here?

Publish as scoped package

Would you be willing to publish this as a scoped package so that one could install only a single flavour?

Importing dependencies like an example in the readme breaks clearIntervalAsync

The following is from the README:

// Choose one of the following flavors: dynamic, fixed, legacy.

import { setIntervalAsync } from 'set-interval-async/dynamic'
import { setIntervalAsync } from 'set-interval-async/fixed'
import { setIntervalAsync } from 'set-interval-async/legacy'
import { clearIntervalAsync } from 'set-interval-async'

When clearIntervalAsync is imported from the root of the project (as shown above) instead of from the same path as whichever "flavor" is chosen, it fails to stop the interval and throws the following error:

Invalid argument: "timer". Expected an intsance of SetIntervalAsyncTimer.

It works if both clearIntervalAsync and setIntervalAsync come from the same path. (also instance is spelled wrong in the error message :) )

Example to reproduce:
https://codesandbox.io/s/elated-wu-w6u93?file=/src/SomeComponent.js

Any race condition ? Possibility to pause while inside the setIntervalAsync ?

Hi,

I am using this, works great. I have it set for 15 seconds. While I am inside the setIntervalAsync my async call (using async / await) can take a while - usually less than 15 seconds but still.. that means there would be a true 15 seconds between runs.

Is there some kind of ability to pause executions while inside setIntervalAsync - so to prevent race conditions and having a true 15 seconds between runs ?

Any idea if this is supported ?

Thanks in advance

Error when running with Webpack 5

When running the library with Webpack 5, there is a problem that prevents compiling:

ERROR in ./node_modules/set-interval-async/fixed/index.mjs 7:0-72
Module not found: Error: Can't resolve '@babel/runtime/helpers/asyncToGenerator' in '/Users/alejandrolopez/NodeJS/notecrow-forms-webapp/node_modules/set-interval-async/fixed'
Did you mean 'asyncToGenerator.js'?
BREAKING CHANGE: The request '@babel/runtime/helpers/asyncToGenerator' failed to resolve only because it was resolved as fully specified
(probably because the origin is strict EcmaScript Module, e. g. a module with javascript mimetype, a '.mjs' file, or a '.js' file where the package.json contains '"type": "module"').
The extension in the request is mandatory for it to be fully specified.
Add the extension to the request.

This can be solved by upgrading the @babel/runtime to version "^7.12.0" (I don't understand why you fix the version to specific one instead of using the ^ version specification).

As a workaround (only works for npm version >=8.3.0) you can add this to your package.json:

  "overrides": {
    "@babel/runtime@": "^7.12.0"
  },

/node_modules/set-interval-async/dist/set-interval-async-timer.cjs:25

image

/node_modules/set-interval-async/dist/set-interval-async-timer.cjs:25 #scheduleTimeout(strategy, handler, intervalMs, delayMs, ...handlerArgs) { ^

SyntaxError: Unexpected token '('

/node_modules/set-interval-async/dist/set-interval-async-timer.cjs:33
async #runHandlerAndScheduleTimeout(strategy, handler, intervalMs, ...handlerArgs) {
^

SyntaxError: Unexpected token '('

Can't import the named export 'clearIntervalAsync' (reexported as 'clearIntervalAsync') from default-exporting module (only default export is available)

Hi, after upgrading all my dependencies in my React (CRA) app, including set-interval-async I now get the error:

Can't import the named export 'clearIntervalAsync' (reexported as 'clearIntervalAsync') from default-exporting module (only default export is available)

when building it. I checked the type definitions to see where that mentioned default export is, but couldn't find any. There are only named exports, so I'm a bit clueless now how to solve this import issue.

I'm using the latest version of set-interval-async (3.0.2), Node.js version 18.12.1 and this is my import line:

import { clearIntervalAsync, setIntervalAsync, SetIntervalAsyncTimer } from "set-interval-async/dynamic";

Cannot be imported to non-TypeScript project after 3.0.0-3.0.2

node:internal/errors:465
    ErrorCaptureStackTrace(err);
    ^

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './fixed/' is not defined by "exports" in /Users/j/code/project/node_modules/set-interval-async/package.json imported from /Users/j/code/project/packages/main/src/index.js
    at new NodeError (node:internal/errors:372:5)
    at throwExportsNotFound (node:internal/modules/esm/resolve:472:9)
    at packageExportsResolve (node:internal/modules/esm/resolve:753:3)
    at packageResolve (node:internal/modules/esm/resolve:935:14)
    at moduleResolve (node:internal/modules/esm/resolve:1003:20)
    at defaultResolve (node:internal/modules/esm/resolve:1218:11)
    at ESMLoader.resolve (node:internal/modules/esm/loader:580:30)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:294:18)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:80:40)
    at link (node:internal/modules/esm/module_job:78:36) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}
error Command failed with exit code 1.

Swallowing errors?

I was considering this library, but it looks like you swallow errors after console logging them.

    try {
      await handler(...args)
    } catch (err) {
      console.error(err)
    }

If my async operation throws an error, I'd want to be handling it myself, and if I forgot to handle it (or didn't want to), I'd rather setIntervalAsync rethrow than log and swallow.

setIntervalAsync stop working after some hours

I have the following code, It works as espected but after some hours the interval stop working

`const { fetchSinToken } = require('../helpers/fetch');
const logger = require('../helpers/logger');
const { setIntervalAsync } = require('set-interval-async/dynamic');

//const setIntervalAsync = require('../helpers/setIntervalAsync');

class Sockets {

constructor( io ) {

    this.io = io;
    this.jobAtmRunning = false;
   
    this.socketEvents();
    
    this.refreshAtms();

}

refreshAtms = () => {
    setIntervalAsync( async() => {
        const date = new Date().toLocaleTimeString();
        try {
            console.log(this.io.engine.clientsCount);
            if(this.io.engine.clientsCount === 0){
                return;
            }
            let resp = await fetchSinToken('atm');
            
            if(resp.status === 200){
                console.log(date, ' - ', 'Enviando ATMs')
                
                this.io.sockets.emit('atms-refresh', resp.data);
            }

            resp = await fetchSinToken('atm/ultimastransacciones');
            if(resp.status === 200){
                console.log(date, ' - ', 'Enviado ultimas transacciones')
                this.io.sockets.emit('atm-ultimas-trx', resp.data);
            }
            
        } catch (error) {
            console.log("Error en consulta", error)            
        }
 
     }, Number(process.env.ATM_REQUEST_INTERVAL));
}


socketEvents() {
    // On connection
    this.io.on('connection', ( socket ) => {
        console.log('Cliente conectado!!');       
    });
}

}

module.exports = Sockets;`

first method for es6 import not work in nodejs

import { setIntervalAsync } from 'set-interval-async/dynamic'

internal/process/esm_loader.js:74
internalBinding('errors').triggerUncaughtException(
^

Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/Users/cheluskin/lf-server/node_modules/set-interval-async/dynamic' is not supported resolving ES modules imported from /Users/cheluskin/lf-server/index.js
Did you mean to import set-interval-async/dynamic/index.js?

Question regarding clearIntervalAsync

Hello,

We are planning to use this application in a React Native application, where performing setInterval or setTimeout with a large interval is discouraged (facebook/react-native#12981). This isn't a problem, except for the following code in clear.js:

  const noopInterval = setInterval(noop, MAX_INTERVAL_MS)
  await Promise.all(promises)
  clearInterval(noopInterval)

Which gives us a warning. Now I'm wondering, what is the purpose of this noopInterval?

It also seems that this noopInterval won't be cleared if any of the promises throw during await Promise.all, so perhaps using a try/finally block would be appropriate.

Thanks!

clear interval from setIntervalAsync inside

For some reason the setIntervalAsync throws an error in this code:

  const checkStatus = async () => {
    const processQueueId = setIntervalAsync(async () => {
      const { value } = await ...
      if (!value.includes(null)) {
          (async () => {
            await clearIntervalAsync(processQueueId);
            setStatuses(value);
            console.log('Stopped!');
          })();
      }
    }, 1000);
  }

processQueueId is not of the type that clearIntervalAsync expect to receive.

Am I doing something wrong?

[Feature Request] Run immediately

Hi,

A nice feature that I am missing from this library is a run immediately before the interval. I currently use a wrapper around your library to make this possible, however, it would be nice to have it built-in.

Thank you

Just leaving the wrapper I'm using in the mean time. I'm aware the dynamic behavior is not enforced until the second call

import { setIntervalAsync, SetIntervalAsyncTimer } from "set-interval-async/dynamic";


const SetIntervalNowAsync = (handler: (...args: any[]) => any, interval: number, ...args: any[]): SetIntervalAsyncTimer => {
  let ready = false;

  Promise.resolve(handler()).then(() => ready = true);

  const safeHandler = () => {
    if (ready) {
      return handler();
    }
  };

  return setIntervalAsync(safeHandler, interval, ...args);
}

export { SetIntervalNowAsync as setIntervalAsync };

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.