Skip to content

Commit

Permalink
2.0 rewrite (typescript, etc.) (#34)
Browse files Browse the repository at this point in the history
* replace npm with pnpm
* replace standard with eslint and prettier
* replace ava with vitest
* use typescript
* replace travis ci with github actions
* clean up README
* document code with tsdoc
* new default (retries: 2)
  • Loading branch information
SEAPUNK authored Apr 7, 2024
1 parent 0c7bb60 commit 684fbcc
Show file tree
Hide file tree
Showing 16 changed files with 3,029 additions and 726 deletions.
13 changes: 13 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
root: true,
env: { "shared-node-browser": true, es2020: true },
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
ignorePatterns: ["lib", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["eslint-plugin-tsdoc"],
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"no-constant-condition": ["error", { checkLoops: false }],
"tsdoc/syntax": "error",
},
};
13 changes: 13 additions & 0 deletions .github/actions/common-setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: "Common setup"
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18

- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
run_install: true
52 changes: 52 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: CI
on:
- push
jobs:
test-style:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- uses: ./.github/actions/common-setup

- name: test:style
run: pnpm run test:style

test-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- uses: ./.github/actions/common-setup

- name: test:lint
run: pnpm run test:lint

test-types:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- uses: ./.github/actions/common-setup

- name: test:types
run: pnpm run test:types

test-vitest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- uses: ./.github/actions/common-setup

- name: test:vitest
run: pnpm run test:vitest

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
node_modules/
npm-debug*.log
.nyc_output/
coverage/
lib/
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
lib/
coverage/
pnpm-lock.yaml
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
7 changes: 0 additions & 7 deletions .travis.yml

This file was deleted.

119 changes: 55 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
recaller
===

[![Greenkeeper badge](https://badges.greenkeeper.io/SEAPUNK/recaller.svg)](https://greenkeeper.io/)

[![npm version](https://img.shields.io/npm/v/recaller.svg?style=flat-square)](https://npmjs.com/package/recaller)
[![javascript standard style](https://img.shields.io/badge/code%20style-standard-blue.svg?style=flat-square)](http://standardjs.com/)
[![travis build](https://img.shields.io/travis/SEAPUNK/recaller/master.svg?style=flat-square)](https://travis-ci.org/SEAPUNK/recaller)
[![coveralls coverage](https://img.shields.io/coveralls/SEAPUNK/recaller.svg?style=flat-square)](https://coveralls.io/github/SEAPUNK/recaller)
[![david dependencies](https://david-dm.org/SEAPUNK/recaller.svg?style=flat-square)](https://david-dm.org/SEAPUNK/recaller)
[![david dev dependencies](https://david-dm.org/SEAPUNK/recaller/dev-status.svg?style=flat-square)](https://david-dm.org/SEAPUNK/recaller)
# recaller

[![npm version](https://img.shields.io/npm/v/recaller.svg)](https://npmjs.com/package/recaller)
[![CI](https://github.com/SEAPUNK/recaller/actions/workflows/main.yml/badge.svg)](https://github.com/SEAPUNK/recaller/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/SEAPUNK/recaller/graph/badge.svg?token=UnnvO670f2)](https://codecov.io/gh/SEAPUNK/recaller)

Promise-based function retry utility. Designed for `async/await`.

**Requires node v4 or above**

`npm install recaller`

- [usage](#usage)
Expand All @@ -24,58 +15,53 @@ Promise-based function retry utility. Designed for `async/await`.

---

usage
---
## usage

*example partially stolen from [async-retry](https://github.com/zeit/async-retry)'s example*
_example partially stolen from [async-retry](https://github.com/zeit/async-retry)'s example_

```js
import recaller from 'recaller'
import fetch from 'node-fetch'

export default async function fetchSomething () {
return await recaller(async (bail, attempt) => {
const res = await fetch('https://google.com')

if (403 === res.status) {
// we're not going to retry
return bail(new Error('Unauthorized'))
}

const data = await res.text()
return data.substr(0, 500)
}, {retries: 5})
import recaller, { constantBackoff } from "recaller";
import fetch from "node-fetch";

export default async function fetchSomething() {
return await recaller(
async (bail, attempt) => {
const res = await fetch("https://google.com");

if (403 === res.status) {
// we're not going to retry
return bail(new Error("Unauthorized"));
}

const data = await res.text();
return data.substr(0, 500);
},
{
// default: 2 retries
retries: 10,
// default: no backoff, retry immediately
backoff: constantBackoff(1000),
},
);
}
```

## api

api
---

`recaller(fn, opts)`

Calls provided (async or regular) function, and retries on failure.

`fn` is called with two arguments:
- `bail(err)` Stops and rejects the retryer's promise with given error.
+ Note that this does not stop execution, so you have to return manually, allowing you to do some cleanup before returning
- `attempt` Current attempt. First call to function = attempt 1.

`opts` is an object, with the following properties:
The code is fully TSDoc'd. See `src/index.ts` for documentation of the main functions, listed below:

- `opts.retries` (default `10`) How many times to retry before giving up, and rejecting with the error.
- `opts.backoff` (default `null`) Backoff generator to use. If null, there is no backoff, and on fail, the function is retried immediately. See: [backoffs](#backoffs)
- `opts.onretry` (default `null`) Retry event handler. See: [handling retries](#handling-retries)
- `recaller(fn, opts)`
- `constantBackoff(ms)`
- `fullJitterBackoff(opts)`

backoffs
---
## backoffs

`recaller` doesn't backoff (wait before retrying) by default. To specify backoff, you must give it a "backoff generator" in the options (`opts.backoff`).
`recaller` doesn't backoff (wait before retrying) by default. To specify backoff, you must give it a backoff function in the options (`opts.backoff`).

example:

```js
import recaller, {constantBackoff} from 'recaller'
import recaller, { constantBackoff } from 'recaller'

export default function doSomething () {
return await recaller(async () => {
Expand All @@ -87,27 +73,32 @@ export default function doSomething () {
}
```

A backoff generator is a function that returns the next delay to wait in milliseconds. For example, the full `constantBackoff(ms)` generator is below:
A backoff function, given an attempt count, returns the next delay to wait in milliseconds.
For example, `constantBackoff(ms)` below:

```js
function constantBackoff (ms) {
ms = ms || 5000
return (attempt) => ms
function constantBackoff(ms) {
ms = ms ?? 5000;
return (attempt) => ms;
}
```
`recaller` comes with 5 backoff generator functions, inspired by [AWS's exponential backoff blog post](https://www.awsarchitectureblog.com/2015/03/backoff.html).
- [`constantBackoff(ms)`](https://github.com/SEAPUNK/recaller/blob/56f9d7b29a0459e1c4f4d40b1de9cd53be589405/lib/index.js#L86-L97)
- [`exponentialBackoff({base, cap, factor})`](https://github.com/SEAPUNK/recaller/blob/56f9d7b29a0459e1c4f4d40b1de9cd53be589405/lib/index.js#L99-L123)
- [`fullJitterBackoff({base, cap, factor})`](https://github.com/SEAPUNK/recaller/blob/56f9d7b29a0459e1c4f4d40b1de9cd53be589405/lib/index.js#L125-L136)
- [`equalJitterBackoff({base, cap, factor})`](https://github.com/SEAPUNK/recaller/blob/56f9d7b29a0459e1c4f4d40b1de9cd53be589405/lib/index.js#L138-L153)
- [`decorrelatedJitterBackoff({base, cap, times})`](https://github.com/SEAPUNK/recaller/blob/56f9d7b29a0459e1c4f4d40b1de9cd53be589405/lib/index.js#L154-L178)
Use `fullJitterBackoff` for most cases, as it generally gives you the best results. You only really have to tweak the `base` and `cap` with it. See code for more documentation.
handling retries
---
- `constantBackoff(ms)`
- `fullJitterBackoff({base, cap, factor})`
the following aren't recommended, and only exist for completeness:
- `exponentialBackoff({base, cap, factor})`
- `equalJitterBackoff({base, cap, factor})`
- `decorrelatedJitterBackoff({base, cap, times})`
## handling retries
You can intercept each retry attempt, by providing a middleware function in `opts.onretry`.
You can intercept each retry attempt, by providing a function in `opts.onretry`.
```js
import recaller from 'recaller'
Expand All @@ -126,7 +117,7 @@ export default function doSomething () {

logger.warn(`doSomething attempt ${attempt} failed;
will wait ${delayTime} ms before trying again.
error: ${err.stack}
error: ${err}
`)
}
})
Expand Down
Loading

0 comments on commit 684fbcc

Please sign in to comment.