Skip to content

Commit

Permalink
feat: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 committed Dec 18, 2017
1 parent 551296c commit 0c32f56
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 19 deletions.
6 changes: 5 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"extends": [
"@jedwards1211/eslint-config", "@jedwards1211/eslint-config-flow"
]
],
"env": {
"shared-node-browser": true,
"commonjs": true
}
}
4 changes: 1 addition & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ cache:
notifications:
email: false
node_js:
- '7'
- '8'
- '6'
- '4'
before_script:
- npm prune
after_success:
- npm run codecov
- npm run semantic-release
Expand Down
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,54 @@
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)

handy promised-based polling API
yet another promise-based poller (I didn't like the API design of others out there)

## Usage
## Example

```sh
npm install --save poll
```

```js
const poll = require('poll')
const superagent = require('superagent')

poll(() => superagent.get('http://google.com'), 1000)
.timeout(30000)
.then(() => console.log("You're connected to the internet!"))
```

## `poll(fn, interval)`

Begins calling `fn` every `interval` milliseconds until the condition passes
(which defaults to `fn` didn't throw an `Error` or return a rejected `Promise`).

Returns a `Promise` that resolves when polling finishes or fails, which may be:
* when `fn` calls the `pass` method provided to it
* when `fn` calls the `fail` method provided to it
* when `fn` returns/resolves to a value or throws/rejects with an `Error` that
passes the condition
* when a timeout is specified and `poll` times out waiting for any of the above

`fn` will be called with a context object:
```js
{
attemptNumber: number, // the number of this call (starting from 0)
elapsedTime: number, // the number of milliseconds since polling started
pass: (value: any) => void, // makes `poll` resolve immediately with this value
fail: (error: Error) => void, // makes `poll` reject immediately with this Error
}
```

You can change the condition by calling `.until` on the returned `Promise`:
```js
poll(...).until((error, result) => result > 3)
```

`error` will be the `Error` from the last call to `fn` (if it rejected or threw)
and `result` will be the value it resolved to or returned otherwise.

You can specify a timeout (in milliseconds) by calling `.timeout` on the returned `Promise`:
```js
poll(...).timeout(30000) // time out after 30 seconds
```
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "poll",
"name": "@jcoreio/poll",
"version": "0.0.0-development",
"description": "handy promised-based polling API",
"main": "lib/index.js",
Expand Down Expand Up @@ -79,6 +79,6 @@
"mocha": "^3.2.0",
"nyc": "^10.1.2",
"rimraf": "^2.6.0",
"semantic-release": "^6.3.6"
"semantic-release": "^8.2.0"
}
}
}
94 changes: 91 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,92 @@
/* @flow */
// @flow

/* eslint-disable no-console, no-undef */
console.log('Hello world!')
export type UntilCondition<T> = (error: ?Error, result?: T) => boolean | Promise<boolean>

export type Poller<T> = Promise<T> & {
cancel(): void;
until(condition: UntilCondition<T>): Poller<T>;
timeout(ms: number): Poller<T>;
}

export type CallContext<T> = {
attemptNumber: number;
elapsedTime: number;
fail(error: Error): void;
pass(value: T): void;
}

function poll<T>(
fn: (info: CallContext<T>) => T | Promise<T>,
interval: number
): Poller<T> {
let fail, pass
let attemptNumber = 0
let until = (error: ?Error, result?: T) => !error
let timeout: ?number
let timeoutId: ?number
let lastError: ?Error

if (!Number.isFinite(interval) || interval < 0) {
throw new Error(`invalid interval: ${interval}`)
}

const promise = new Promise((resolve: (value: T) => void, reject: (error: Error) => void) => {
const startTime = Date.now()

fail = (error: Error) => {
if (timeoutId != null) clearTimeout(timeoutId)
reject(error)
}
pass = (value: T) => {
if (timeoutId != null) clearTimeout(timeoutId)
resolve(value)
}

async function attempt(): Promise<void> {
let result, error
const now = Date.now()
try {
result = await fn({
attemptNumber: attemptNumber++,
elapsedTime: now - startTime,
fail,
pass,
})
} catch (err) {
lastError = error = err
}

if (await until(error, result)) {
if (error) reject(error)
else resolve((result: any))
}
else {
const nextTime = now + interval
if (timeout != null && nextTime - startTime > timeout) {
let message = "timed out waiting for polling to succeed"
if (lastError) message += `; last error: ${lastError.stack}`
reject(new Error(message))
} else {
const delay = Math.max(0, nextTime - Date.now())
timeoutId = setTimeout(attempt, delay)
}
}
}

attempt()
})

;(promise: any).cancel = () => fail(new Error("polling canceled"))
;(promise: any).until = (condition: UntilCondition<T>) => {
until = condition
return promise
}
;(promise: any).timeout = (ms: number) => {
timeout = ms
return promise
}

return (promise: any)
}

module.exports = poll
4 changes: 0 additions & 4 deletions src/index.js.flow

This file was deleted.

79 changes: 76 additions & 3 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,80 @@
import '../src/index'
// @flow

describe('test setup', () => {
it('works', () => {
import {describe, it} from 'mocha'
import poll from '../src'
import {expect} from 'chai'

import type {CallContext} from '../src'

describe('poll', function () {
this.timeout(2000)
it('throws when interval is missing', async () => {
// $FlowFixMe
expect(() => poll(() => {})).to.throw(Error)
})
it('throws when interval is NaN', async () => {
expect(() => poll(() => {}, NaN)).to.throw(Error)
})
it('throws when interval is negative', async () => {
expect(() => poll(() => {}, -1)).to.throw(Error)
})
it('resolves when condition is met', async () => {
let numAttempts
await poll(({attemptNumber, elapsedTime}: CallContext<void>) => {
numAttempts = attemptNumber + 1
if (elapsedTime < 250) throw new Error()
}, 100).timeout(1000)
expect(numAttempts).to.equal(4)
})
it('rejects when condition times out', async () => {
let numAttempts
let error
await poll(({attemptNumber, elapsedTime}: CallContext<void>) => {
numAttempts = attemptNumber + 1
if (elapsedTime < 500) throw new Error('test!')
}, 100).timeout(250).catch(err => error = err)
expect(numAttempts).to.equal(3)
if (!error) throw new Error('expected error to be thrown')
expect(error.message).to.match(/timed out/i)
expect(error.message).to.match(/last error: Error: test!/i)
})
it('allows fn to manually pass', async () => {
const result = await poll(({attemptNumber, pass}: CallContext<number>) => {
if (attemptNumber === 3) pass(attemptNumber)
}, 20).until((error, value) => value != null)
expect(result).to.equal(3)
})
it('allows fn to manually fail', async () => {
let numAttempts
let error
await poll(({attemptNumber, elapsedTime, fail}: CallContext<void>) => {
numAttempts = attemptNumber + 1
if (elapsedTime < 50) throw new Error()
else fail(new Error('manually failed!'))
}, 20).catch(err => error = err)
expect(numAttempts).to.equal(4)
if (!error) throw new Error('expected error to be thrown')
expect(error.message).to.equal('manually failed!')
})
it('allows until condition to be overridden', async () => {
const value = await poll(({attemptNumber}: CallContext<number>) => attemptNumber, 20)
.until((error, value) => value === 3)
expect(value).to.equal(3)
})
it('throws if condition becomes true on error', async () => {
let error
await poll(({attemptNumber}: CallContext<void>) => {
if (attemptNumber === 3) throw new Error('done!')
}, 20).until(error => Boolean(error)).catch(err => error = err)
if (!error) throw new Error('expected error to be thrown')
expect(error.message).to.equal('done!')
})
it('rejects when canceled', async () => {
let error
const promise = poll(() => { throw new Error() }, 20)
promise.cancel()
await promise.catch(err => error = err)
if (!error) throw new Error('expected error to be thrown')
expect(error.message).to.match(/canceled/)
})
})

0 comments on commit 0c32f56

Please sign in to comment.