Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/9oelm/async-jobs

For the paranoids of async jobs in javascript - track, manage, access all async jobs like network requests on browser at one place.
https://github.com/9oelm/async-jobs

async network

Last synced: 17 days ago
JSON representation

For the paranoids of async jobs in javascript - track, manage, access all async jobs like network requests on browser at one place.

Awesome Lists containing this project

README

        

# async-jobs

_For the paranoids of async jobs in javascript._

- Track, manage, access all async jobs.
- Best used for multiple, long running, error-prone jobs (network requests, web workers, etc).
- Strictly typed everywhere with Typescript.
- Usable for both Redux and non-redux applications.
- Recipes included.

# Install

```
npm i --save @async-jobs/core
```

```
yarn add @async-jobs/core
```

# Demo

https://9oelm.github.io/async-jobs/

# Full API Reference

https://async-jobs-api-docs.surge.sh/

# Usage with Redux

Before you begin, you need to create `async` reducer in your existing redux store. This is going to be the single source of truth for all of your async jobs:

```javascript
import { combineReducers, createStore } from "redux"
import { asyncReducer } from '@async-jobs/core';

const rootReducer = combineReducers({
async: asyncReducer,
otherReducers: reducer1,
...
})

export const store = createStore(rootReducer)
```

## The 'Vanilla Redux' way

[👉 Run this example on your computer](https://github.com/9oelM/async-jobs/tree/main/packages/vanilla-redux-example)

This is the most fundamental way to use `async-jobs` in a Redux application, although it is not the recommended way. This is not recommended because side effects reside in the component but redux middleware, which means it is almost equivalent to using a local `useState`. But it does the job - it can track the async request and it is stored in redux, accessible from anywhere else too. You get the idea.

```javascript
import React, { useEffect, useRef } from "react"
import { useDispatch } from "react-redux"
import {
asyncJobByIdSelector,
AsyncStatus,
createJobSet,
} from "@async-jobs/core"
import { TcResult, tcAsync } from "./utilities/essentials"
import { useTypedSelector } from "."

export class API {
static baseUrl = `https://example-api-six.vercel.app`

static async bookFlightTicket({
timeout_secs = 0,
make_error = false,
}: {
timeout_secs?: number
make_error?: boolean
}): Promise> {
return tcAsync(
window
.fetch(
`${API.baseUrl}/api/main?timeout_secs=${timeout_secs}&make_error=${make_error}`
)
.then((response) => {
if (response.ok) {
return response.text()
} else {
throw new Error(`Error happened: ${response.statusText}`)
}
})
)
}
}

enum AsyncJobNames {
POST_BOOK_FLIGHT_TICKET = `POST_BOOK_FLIGHT_TICKET`,
}

export const postBookFlightTicketJobSet = createJobSet<
AsyncJobNames.POST_BOOK_FLIGHT_TICKET,
{
destination: string
username: string
},
{
destination: string
username: string
},
string,
Error,
void
>(AsyncJobNames.POST_BOOK_FLIGHT_TICKET)

export const BookFlightPage: React.FC<{
destination: string
username: string
}> = ({ destination, username }) => {
const bookFlightTicketCreatedJob = useRef(
postBookFlightTicketJobSet.create({
payload: {
destination,
username,
},
})
)
const dispatch = useDispatch()
const currentAsyncJob = useTypedSelector((s) =>
asyncJobByIdSelector(s, bookFlightTicketCreatedJob.current.id)
)

useEffect(() => {
async function bookFlightTicketOnMount() {
dispatch(bookFlightTicketCreatedJob.current)
await new Promise((resolve) => {
setTimeout(resolve, 3000)
})
dispatch(
postBookFlightTicketJobSet.start({
id: bookFlightTicketCreatedJob.current.id,
payload: bookFlightTicketCreatedJob.current.payload,
})
)
const [err, postBookFlightTicketResult] = await API.bookFlightTicket({
timeout_secs: 5,
make_error: false,
})

if (!err && postBookFlightTicketResult) {
dispatch(
postBookFlightTicketJobSet.succeed({
id: bookFlightTicketCreatedJob.current.id,
payload: postBookFlightTicketResult,
})
)
} else {
dispatch(
postBookFlightTicketJobSet.fail({
id: bookFlightTicketCreatedJob.current.id,
payload: err ?? new Error(`unknown error`),
})
)
}
}
bookFlightTicketOnMount()
}, [])

return (


{(() => {
switch (currentAsyncJob?.status) {
case AsyncStatus.CREATED:
return
Created

case AsyncStatus.PENDING:
return
Pending

case AsyncStatus.SUCCESS:
return
Success

case AsyncStatus.FAILURE:
return
Failed

case AsyncStatus.CANCELLED:
return
Cancelled

default:
return
Unknown

}
})()}

)
}
```

## The 'Vanilla Redux middleware' way
This method uses vanilla redux middleware without any other dependencies like `redux-thunk`. Not so many people will want to use this method because it is not very much extensible, but it is still a good option.

First, create `postBookFlightTicketMiddleware`:

```js
import { AsyncJobActions, createJobSet } from '@async-jobs/core'

export const AsyncJobNames = Object.freeze({
POST_BOOK_FLIGHT_TICKET: `POST_BOOK_FLIGHT_TICKET`,
})

export const postBookFlightTicketJobSet = createJobSet(AsyncJobNames.POST_BOOK_FLIGHT_TICKET)

const postBookFlightTicketMiddleware = store => next => action => {
if (!isSpecificAsyncActionType(action, AsyncJobActions.START, AsyncJobNames.POST_BOOK_FLIGHT_TICKET)) {
return next(action)
}
const { payload, id } = action

API.bookFlightTicket({ method: `POST`, body: payload })
.then((postBookFlightTicketResult) => {
next(postBookFlightTicketJobSet.succeed({ id, payload: postBookFlightTicketResult }))
})
.catch((err) => {
next(postBookFlightTicketJobSet.fail({ id, payload: err }))
})

return next(action)
}
```

Then, insert the middleware into your store:

```javascript
import { combineReducers, createStore } from "redux"
import { asyncReducer } from '@async-jobs/core';
import { postBookFlightTicketMiddleware } from './postBookFlightTicketMiddleware'

const rootReducer = combineReducers({
async: asyncReducer,
otherReducers: reducer1,
...
})

const store = createStore(
rootReducer,
applyMiddleware(
postBookFlightTicketMiddleware,
)
)
```

Now, all you have to do is to subscribe to the redux store in your component:

```js
import React from 'react'
import { useDispatch } from 'react-redux'
import { asyncJobByIdSelector } from '@async-jobs/core'

const BookFlightPage = ({ destination, username }) => {
const dispatch = useDispatch()
const startPostBookFlightTicketRequest = useRef(postBookFlightTicketJobSet.start({
payload: {
destination,
username,
}
}))
const currentAsyncJob = useSelector((s) => asyncJobByIdSelector(s, startPostBookFlightTicketRequest.current.id))

useEffect(() => {
dispatch(startPostBookFlightTicketRequest)
}, [])

return

{(() => {
switch (currentAsyncJob.status) {
case AsyncJobStatus.PENDING:
return
Pending

case AsyncJobStatus.SUCCESS:
return
Success

case AsyncJobStatus.FAIL:
return
Failed

case AsyncJobStatus.CANCELED:
return
Canceled

default:
return
Unknown

}
})()}

}
```

Note that in some cases you may not even need to reference the async job id. All you need to know is the name of the async job. Using `createLatestAsyncJobByNameSelector` will do, like so:

```js
import React from 'react'
import { useDispatch } from 'react-redux'
import { createLatestAsyncJobByNameSelector } from "@async-jobs/core";

const latestAsyncJobByNameSelector = createLatestAsyncJobByNameSelector()

// look at how clean we've become regarding the side effects
const BookFlightPage = ({ destination, username }) => {
const dispatch = useDispatch()
const currentAsyncJob = useSelector((s) => latestAsyncJobByNameSelector(s, {
name: AsyncJobNames.POST_BOOK_FLIGHT_TICKET,
}))

useEffect(() => {
dispatch(postBookFlightTicketJobSet.start({
payload: {
destination,
username,
}
}))
}, [
destination,
username,
])

return

{(() => {
switch (currentAsyncJob.status) {
case AsyncJobStatus.PENDING:
return
Pending

case AsyncJobStatus.SUCCESS:
return
Success

case AsyncJobStatus.FAIL:
return
Failed

case AsyncJobStatus.CANCELED:
return
Canceled

default:
return
Unknown

}
})()}

}
```

## The `redux-thunk` way

WIP

# Typescript

`async-jobs` is fully made with an obsession with Typescript. Autocompletion will help you get things right.
For more, please see full API reference.