{"id":15935909,"url":"https://github.com/9oelm/async-jobs","last_synced_at":"2026-02-07T12:31:13.312Z","repository":{"id":44963501,"uuid":"446669378","full_name":"9oelM/async-jobs","owner":"9oelM","description":"For the paranoids of async jobs in javascript - track, manage, access all async jobs like network requests on browser at one place.","archived":false,"fork":false,"pushed_at":"2022-01-15T13:05:36.000Z","size":6300,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-15T02:52:15.156Z","etag":null,"topics":["async","network"],"latest_commit_sha":null,"homepage":"https://9oelm.github.io/async-jobs","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/9oelM.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-01-11T03:51:24.000Z","updated_at":"2022-09-02T13:15:20.000Z","dependencies_parsed_at":"2022-08-20T17:31:44.769Z","dependency_job_id":null,"html_url":"https://github.com/9oelM/async-jobs","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":"9oelM/react-typescript-monorepo-boilerplate","purl":"pkg:github/9oelM/async-jobs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/9oelM%2Fasync-jobs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/9oelM%2Fasync-jobs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/9oelM%2Fasync-jobs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/9oelM%2Fasync-jobs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/9oelM","download_url":"https://codeload.github.com/9oelM/async-jobs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/9oelM%2Fasync-jobs/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267610412,"owners_count":24115435,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-28T02:00:09.689Z","response_time":68,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","network"],"created_at":"2024-10-07T04:02:44.785Z","updated_at":"2026-02-07T12:31:13.284Z","avatar_url":"https://github.com/9oelM.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# async-jobs\n\n_For the paranoids of async jobs in javascript._\n\n- Track, manage, access all async jobs.\n- Best used for multiple, long running, error-prone jobs (network requests, web workers, etc).\n- Strictly typed everywhere with Typescript. \n- Usable for both Redux and non-redux applications.\n- Recipes included.\n\n# Install \n\n```\nnpm i --save @async-jobs/core\n```\n\n```\nyarn add @async-jobs/core\n```\n\n# Demo\n\nhttps://9oelm.github.io/async-jobs/\n\n# Full API Reference\n\nhttps://async-jobs-api-docs.surge.sh/\n\n# Usage with Redux\n\nBefore 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:\n\n```javascript\nimport { combineReducers, createStore } from \"redux\"\nimport { asyncReducer } from '@async-jobs/core';\n\nconst rootReducer = combineReducers({\n  async: asyncReducer,\n  otherReducers: reducer1,\n  ...\n})\n\nexport const store = createStore(rootReducer)\n```\n\n## The 'Vanilla Redux' way\n\n[👉 Run this example on your computer](https://github.com/9oelM/async-jobs/tree/main/packages/vanilla-redux-example)\n\nThis 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.\n\n```javascript\nimport React, { useEffect, useRef } from \"react\"\nimport { useDispatch } from \"react-redux\"\nimport {\n  asyncJobByIdSelector,\n  AsyncStatus,\n  createJobSet,\n} from \"@async-jobs/core\"\nimport { TcResult, tcAsync } from \"./utilities/essentials\"\nimport { useTypedSelector } from \".\"\n\nexport class API {\n  static baseUrl = `https://example-api-six.vercel.app`\n\n  static async bookFlightTicket({\n    timeout_secs = 0,\n    make_error = false,\n  }: {\n    timeout_secs?: number\n    make_error?: boolean\n  }): Promise\u003cTcResult\u003cstring, Error\u003e\u003e {\n    return tcAsync(\n      window\n        .fetch(\n          `${API.baseUrl}/api/main?timeout_secs=${timeout_secs}\u0026make_error=${make_error}`\n        )\n        .then((response) =\u003e {\n          if (response.ok) {\n            return response.text()\n          } else {\n            throw new Error(`Error happened: ${response.statusText}`)\n          }\n        })\n    )\n  }\n}\n\nenum AsyncJobNames {\n  POST_BOOK_FLIGHT_TICKET = `POST_BOOK_FLIGHT_TICKET`,\n}\n\nexport const postBookFlightTicketJobSet = createJobSet\u003c\n  AsyncJobNames.POST_BOOK_FLIGHT_TICKET,\n  {\n    destination: string\n    username: string\n  },\n  {\n    destination: string\n    username: string\n  },\n  string,\n  Error,\n  void\n\u003e(AsyncJobNames.POST_BOOK_FLIGHT_TICKET)\n\nexport const BookFlightPage: React.FC\u003c{\n  destination: string\n  username: string\n}\u003e = ({ destination, username }) =\u003e {\n  const bookFlightTicketCreatedJob = useRef(\n    postBookFlightTicketJobSet.create({\n      payload: {\n        destination,\n        username,\n      },\n    })\n  )\n  const dispatch = useDispatch()\n  const currentAsyncJob = useTypedSelector((s) =\u003e\n    asyncJobByIdSelector(s, bookFlightTicketCreatedJob.current.id)\n  )\n\n  useEffect(() =\u003e {\n    async function bookFlightTicketOnMount() {\n      dispatch(bookFlightTicketCreatedJob.current)\n      await new Promise((resolve) =\u003e {\n        setTimeout(resolve, 3000)\n      })\n      dispatch(\n        postBookFlightTicketJobSet.start({\n          id: bookFlightTicketCreatedJob.current.id,\n          payload: bookFlightTicketCreatedJob.current.payload,\n        })\n      )\n      const [err, postBookFlightTicketResult] = await API.bookFlightTicket({\n        timeout_secs: 5,\n        make_error: false,\n      })\n\n      if (!err \u0026\u0026 postBookFlightTicketResult) {\n        dispatch(\n          postBookFlightTicketJobSet.succeed({\n            id: bookFlightTicketCreatedJob.current.id,\n            payload: postBookFlightTicketResult,\n          })\n        )\n      } else {\n        dispatch(\n          postBookFlightTicketJobSet.fail({\n            id: bookFlightTicketCreatedJob.current.id,\n            payload: err ?? new Error(`unknown error`),\n          })\n        )\n      }\n    }\n    bookFlightTicketOnMount()\n  }, [])\n\n  return (\n    \u003cdiv\u003e\n      {(() =\u003e {\n        switch (currentAsyncJob?.status) {\n          case AsyncStatus.CREATED:\n            return \u003cdiv\u003eCreated\u003c/div\u003e\n          case AsyncStatus.PENDING:\n            return \u003cdiv\u003ePending\u003c/div\u003e\n          case AsyncStatus.SUCCESS:\n            return \u003cdiv\u003eSuccess\u003c/div\u003e\n          case AsyncStatus.FAILURE:\n            return \u003cdiv\u003eFailed\u003c/div\u003e\n          case AsyncStatus.CANCELLED:\n            return \u003cdiv\u003eCancelled\u003c/div\u003e\n          default:\n            return \u003cdiv\u003eUnknown\u003c/div\u003e\n        }\n      })()}\n    \u003c/div\u003e\n  )\n}\n```\n\n## The 'Vanilla Redux middleware' way\nThis 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.\n\nFirst, create `postBookFlightTicketMiddleware`:\n\n```js\nimport { AsyncJobActions, createJobSet } from '@async-jobs/core'\n\nexport const AsyncJobNames = Object.freeze({\n  POST_BOOK_FLIGHT_TICKET: `POST_BOOK_FLIGHT_TICKET`,\n})\n\nexport const postBookFlightTicketJobSet = createJobSet(AsyncJobNames.POST_BOOK_FLIGHT_TICKET)\n\nconst postBookFlightTicketMiddleware = store =\u003e next =\u003e action =\u003e {\n  if (!isSpecificAsyncActionType(action, AsyncJobActions.START, AsyncJobNames.POST_BOOK_FLIGHT_TICKET)) {\n    return next(action)\n  }\n  const { payload, id } = action\n\n  API.bookFlightTicket({ method: `POST`, body: payload })\n    .then((postBookFlightTicketResult) =\u003e {\n        next(postBookFlightTicketJobSet.succeed({ id, payload: postBookFlightTicketResult }))\n    })\n    .catch((err) =\u003e {\n        next(postBookFlightTicketJobSet.fail({ id, payload: err }))\n    })\n\n  return next(action)\n}\n```\n\nThen, insert the middleware into your store:\n\n```javascript\nimport { combineReducers, createStore } from \"redux\"\nimport { asyncReducer } from '@async-jobs/core';\nimport { postBookFlightTicketMiddleware } from './postBookFlightTicketMiddleware'\n\nconst rootReducer = combineReducers({\n  async: asyncReducer,\n  otherReducers: reducer1,\n  ...\n})\n\nconst store = createStore(\n  rootReducer,\n  applyMiddleware(\n    postBookFlightTicketMiddleware,\n  )\n)\n```\n\nNow, all you have to do is to subscribe to the redux store in your component:\n\n```js\nimport React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { asyncJobByIdSelector } from '@async-jobs/core'\n\nconst BookFlightPage = ({ destination, username }) =\u003e {\n  const dispatch = useDispatch()\n  const startPostBookFlightTicketRequest = useRef(postBookFlightTicketJobSet.start({\n    payload: {\n      destination,\n      username,\n    }\n  }))\n  const currentAsyncJob = useSelector((s) =\u003e asyncJobByIdSelector(s, startPostBookFlightTicketRequest.current.id))\n\n  useEffect(() =\u003e {\n    dispatch(startPostBookFlightTicketRequest)\n  }, [])\n  \n  return \u003cdiv\u003e{(() =\u003e {\n    switch (currentAsyncJob.status) {\n      case AsyncJobStatus.PENDING:\n        return \u003cdiv\u003ePending\u003c/div\u003e\n      case AsyncJobStatus.SUCCESS:\n        return \u003cdiv\u003eSuccess\u003c/div\u003e\n      case AsyncJobStatus.FAIL:\n        return \u003cdiv\u003eFailed\u003c/div\u003e\n      case AsyncJobStatus.CANCELED:\n        return \u003cdiv\u003eCanceled\u003c/div\u003e\n      default:\n        return \u003cdiv\u003eUnknown\u003c/div\u003e\n    }\n  })()}\u003c/div\u003e\n}\n```\n\nNote 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:\n\n```js\nimport React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { createLatestAsyncJobByNameSelector } from \"@async-jobs/core\";\n\nconst latestAsyncJobByNameSelector = createLatestAsyncJobByNameSelector()\n\n// look at how clean we've become regarding the side effects\nconst BookFlightPage = ({ destination, username }) =\u003e {\n  const dispatch = useDispatch()\n  const currentAsyncJob = useSelector((s) =\u003e latestAsyncJobByNameSelector(s, {\n    name: AsyncJobNames.POST_BOOK_FLIGHT_TICKET,\n  }))\n\n  useEffect(() =\u003e {\n    dispatch(postBookFlightTicketJobSet.start({\n      payload: {\n        destination,\n        username,\n      }\n    }))\n  }, [\n    destination,\n    username,\n  ])\n  \n  return \u003cdiv\u003e{(() =\u003e {\n    switch (currentAsyncJob.status) {\n      case AsyncJobStatus.PENDING:\n        return \u003cdiv\u003ePending\u003c/div\u003e\n      case AsyncJobStatus.SUCCESS:\n        return \u003cdiv\u003eSuccess\u003c/div\u003e\n      case AsyncJobStatus.FAIL:\n        return \u003cdiv\u003eFailed\u003c/div\u003e\n      case AsyncJobStatus.CANCELED:\n        return \u003cdiv\u003eCanceled\u003c/div\u003e\n      default:\n        return \u003cdiv\u003eUnknown\u003c/div\u003e\n    }\n  })()}\u003c/div\u003e\n}\n```\n\n## The `redux-thunk` way\n\nWIP\n\n# Typescript\n\n`async-jobs` is fully made with an obsession with Typescript. Autocompletion will help you get things right.\nFor more, please see full API reference.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F9oelm%2Fasync-jobs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F9oelm%2Fasync-jobs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F9oelm%2Fasync-jobs/lists"}