{"id":13756527,"url":"https://github.com/yumauri/effector-reeffect","last_synced_at":"2025-05-10T03:32:15.113Z","repository":{"id":37885116,"uuid":"225811428","full_name":"yumauri/effector-reeffect","owner":"yumauri","description":"Concurrent effects for Effector ☄️","archived":true,"fork":false,"pushed_at":"2022-08-16T09:44:03.000Z","size":953,"stargazers_count":55,"open_issues_count":1,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-02-11T15:54:52.956Z","etag":null,"topics":["concurrent","effector","effects","side-effects"],"latest_commit_sha":null,"homepage":null,"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/yumauri.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-12-04T07:59:41.000Z","updated_at":"2024-02-11T15:54:52.957Z","dependencies_parsed_at":"2022-08-19T15:10:21.325Z","dependency_job_id":null,"html_url":"https://github.com/yumauri/effector-reeffect","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yumauri%2Feffector-reeffect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yumauri%2Feffector-reeffect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yumauri%2Feffector-reeffect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yumauri%2Feffector-reeffect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yumauri","download_url":"https://codeload.github.com/yumauri/effector-reeffect/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224911584,"owners_count":17390840,"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","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":["concurrent","effector","effects","side-effects"],"created_at":"2024-08-03T11:00:46.224Z","updated_at":"2024-11-16T11:31:49.914Z","avatar_url":"https://github.com/yumauri.png","language":"TypeScript","readme":"# THIS PROJECT IS UNMAINTAINED AND DEPRECATED\n\nPlease use something else. See: https://github.com/yumauri/effector-reeffect/issues/26\n\n# effector-reeffect\n\n[![Build Status](https://github.com/yumauri/effector-reeffect/workflows/build/badge.svg)](https://github.com/yumauri/effector-reeffect/actions?workflow=build)\n[![Coverage Status](https://coveralls.io/repos/github/yumauri/effector-reeffect/badge.svg)](https://coveralls.io/github/yumauri/effector-reeffect)\n[![License](https://img.shields.io/github/license/yumauri/effector-reeffect.svg?color=yellow)](./LICENSE)\n[![NPM](https://img.shields.io/npm/v/effector-reeffect.svg)](https://www.npmjs.com/package/effector-reeffect)\n![Made with Love](https://img.shields.io/badge/made%20with-❤-red.svg)\n\nReEffects for [Effector](https://github.com/zerobias/effector) ☄️\u003cbr\u003e\nLike regular Effects, but better :)\n\n- Supports different launch strategies: TAKE_FIRST, TAKE_LAST, TAKE_EVERY, QUEUE, RACE\n- Handles promises cancellation\n- Can handle _logic_ cancellation\n\n## Table of Contents\n\n\u003c!-- npx markdown-toc README.md --\u003e\n\n- [Install](#install)\n- [Usage](#usage)\n- [Strategies](#strategies)\n  - [TAKE_EVERY](#take_every)\n  - [TAKE_FIRST](#take_first)\n  - [TAKE_LAST](#take_last)\n  - [QUEUE](#queue)\n  - [RACE](#race)\n- [Properties](#properties)\n- [Options](#options)\n- [Cancellation](#cancellation)\n  - [axios](#axios)\n  - [fetch](#fetch)\n  - [ky](#ky)\n  - [request](#request)\n  - [XMLHttpRequest](#xmlhttprequest)\n- [FAQ](#faq)\n- [Sponsored](#sponsored)\n\n## Install\n\n```bash\n$ yarn add effector-reeffect\n```\n\nOr using `npm`\n\n```bash\n$ npm install --save effector-reeffect\n```\n\n## Usage\n\nIn basic version you can use it like regular [Effect](https://effector.now.sh/en/api/effector/effect):\n\n```javascript\nimport { createReEffect } from 'effector-reeffect'\n\n// create ReEffect\nconst fetchUser = createReEffect({\n  handler: ({ id }) =\u003e\n    fetch(`https://example.com/users/${id}`).then(res =\u003e res.json()),\n})\n```\n\nNothing special yet, created ReEffect has same properties, as usual Effect: Events `done`, `fail`, `finally`; Store `pending`; and methods `use(handler)`, `watch(watcher)`, `prepend(fn)`. Check out [documentation](https://effector.now.sh/en/api/effector/effect) to learn more.\n\nMagic begins when you call ReEffect more that once, while previous asynchronous operation is not finished yet 🧙‍♂️\n\n```javascript\nimport { createReEffect, TAKE_LAST } from 'effector-reeffect'\n\n// create ReEffect\nconst fetchUser = createReEffect({\n  handler: ({ id }) =\u003e\n    fetch(`https://example.com/users/${id}`).then(res =\u003e res.json()),\n})\n\n// call it once\nfetchUser({ id: 1 }, TAKE_LAST)\n\n// and somewhere in a galaxy far, far away\nfetchUser({ id: 1 }, TAKE_LAST)\n```\n\nYou see the new `TAKE_LAST` argument - this is called _strategy_, and _TAKE_LAST_ one ensures, that only latest call will trigger `done` (or `fail`) event. Each call still remain separate Promise, so you can _await_ it, but first one in the example above will be rejected with `CancelledError` instance (you can import this class from package and check `error instanceof CancelledError`).\n\n## Strategies\n\n### TAKE_EVERY\n\nThis is _default strategy_, if you will not specify any other.\n\n\u003cimg width=\"475\" alt=\"TAKE_EVERY\" src=\"https://github.com/yumauri/effector-reeffect/blob/master/images/TAKE_EVERY.png?raw=true\"\u003e\n\nSecond effect call will launch second asynchronous operation. In contrast with usual Effect, ReEffect will trigger `.done` (or `.fail`) event only for latest operation, and `.pending` store will contain `true` for a whole time of all operations (in other words, if there is at least single pending operation - `.pending` will hold `true`).\n\n### TAKE_FIRST\n\n\u003cimg width=\"355\" alt=\"TAKE_FIRST\" src=\"https://github.com/yumauri/effector-reeffect/blob/master/images/TAKE_FIRST.png?raw=true\"\u003e\n\nSecond effect call will be immediately rejected with `CancelledError` (handler will not be executed at all).\n\n### TAKE_LAST\n\n\u003cimg width=\"475\" alt=\"TAKE_LAST\" src=\"https://github.com/yumauri/effector-reeffect/blob/master/images/TAKE_LAST.png?raw=true\"\u003e\n\nSecond effect call will reject all currently pending operations (if any) with `CancelledError`.\n\n### QUEUE\n\n\u003cimg width=\"539\" alt=\"QUEUE\" src=\"https://github.com/yumauri/effector-reeffect/blob/master/images/QUEUE.png?raw=true\"\u003e\n\nSecond effect will not be launched until all other pending effects are finished.\n\n### RACE\n\n\u003cimg width=\"409\" alt=\"RACE\" src=\"https://github.com/yumauri/effector-reeffect/blob/master/images/RACE.png?raw=true\"\u003e\n\nFirst finished effect will win the race and cancel all other pending effects with `CancelledError`.\n\nThis strategy is a bit different, then first four. You can call them \"**→IN** strategies\", while RACE is \"**OUT→** strategy\".\n\nReEffect checks **→IN** strategy in the moment effect was launched. Effect, launched with _TAKE_LAST_ strategy, will cancel all currently pending effects, regardless of their strategies. Effect, launched with _QUEUE_ strategy, will be placed in queue to wait all currently pending effects, regardless of their strategies. And so on.\n\n**OUT→** strategy is checked, when effect is fulfilled (but not cancelled). Effect with _RACE_ strategy, upon finished, will cancel all other pending effects, regardless of their strategies.\n\nIt should be noted, that due to asynchronous cancellation, `cancelled` events for loser effects will happen _after_ main `done`/`fail` event, and _after_ `pending` is set to `false`.\n\n## Properties\n\nReEffect has few new properties:\n\n- `.cancelled`: Event triggered when any handler is rejected with `CancelledError` or `LimitExceededError` (this will be described later).\n- `.cancel`: Event you can trigger to manually cancel all currently pending operations - each cancelled operation will trigger `.cancelled` event.\n\n## Options\n\n`createReEffect` function accepts same arguments as usual Effect, with few possible additions in config:\n\n- `strategy`: this strategy will be considered as default, instead of `TAKE_EVERY`. Possible values: `TAKE_EVERY`, `TAKE_FIRST`, `TAKE_LAST`, `QUEUE` or `RACE`.\n- `feedback`: if `true` — puts `strategy` field into `done`, `fail` or `cancelled` event's payload. With `false` by default ReEffect behaves just like usual Effect, with exactly the same results.\n- `limit`: maximum count of simultaneously running operation, by default `Infinity`. If new effect call will exceed this value, call will be immediately rejected with `LimitExceededError` error.\n- `timeout`: timeout for effect execution, in milliseconds. If timeout is exceeded — effect will be rejected with `TimeoutError`.\n\n```javascript\nconst fetchUser = createReEffect('fetchUser', {\n  handler: ({ id }) =\u003e\n    fetch(`https://example.com/users/${id}`).then(res =\u003e res.json()),\n  strategy: TAKE_LAST,\n  feedback: true,\n  limit: 3,\n  timeout: 5000,\n})\n```\n\nReEffect, created with `createReEffect` function, behave like usual Effect, with one difference: in addition to effect's `payload` you can specify _strategy_ as a second argument. This strategy will override default strategy for this effect (but will not replace default strategy).\n\nYou can also specify config object, with `strategy` or/and `timeout`.\n\n```javascript\n// this are equivalent calls\nfetchUser({ id: 2 }, TAKE_EVERY)\nfetchUser({ id: 2 }, { strategy: TAKE_EVERY })\nfetchUser({ params: { id: 2 }, strategy: TAKE_EVERY })\n\n// or if your effect doesn't have payload\nfetchAllUsers(undefined, RACE)\nfetchAllUsers(undefined, { strategy: RACE })\nfetchAllUsers({ strategy: RACE })\n\n// with timeout\nfetchUser({ id: 42 }, { timeout: 5000 })\nfetchUser({ params: { id: 42 }, strategy: TAKE_EVERY, timeout: 5000 })\n```\n\n## Cancellation\n\nReEffect will handle Promises cancellation for you (so handler promise result will be ignored), _but it cannot cancel logic_ by itself! There are quite an amount of possible asynchronous operations, and each one could be cancelled differently (and some could not be cancelled at all).\n\nBut bright side of it is that you can tell ReEffect, _how to cancel your logic_ ☀️\n\nTo do this, `handler` accepts `onCancel` callback as a second argument, and you can specify, what actually to do on cancel.\n\nLet me show an example:\n\n```javascript\nimport { createReEffect, TAKE_LAST } from 'effector-reeffect'\n\nconst reeffect = createReEffect({ strategy: TAKE_LAST })\n\nreeffect.watch(_ =\u003e console.log('reeffect called:', _))\nreeffect.done.watch(_ =\u003e console.log('reeffect done:', _))\nreeffect.fail.watch(_ =\u003e console.log('reeffect fail:', _))\nreeffect.cancelled.watch(_ =\u003e console.log('reeffect cancelled:', _))\n\nreeffect.use(\n  params =\u003e\n    new Promise(resolve =\u003e {\n      setTimeout(() =\u003e {\n        console.log(`-\u003e AHA! TIMEOUT FROM EFFECT WITH PARAMS: ${params}`)\n        resolve('done')\n      }, 1000)\n    })\n)\n\nreeffect(1)\nreeffect(2)\n```\n\nIf you will run code above, you will get\n\n```\nreeffect called: 1\nreeffect called: 2\nreeffect cancelled: { params: 1,\n  error: Error: Cancelled due to \"TAKE_LAST\", new effect was added }\n-\u003e AHA! TIMEOUT FROM EFFECT WITH PARAMS: 1\n-\u003e AHA! TIMEOUT FROM EFFECT WITH PARAMS: 2\nreeffect done: { params: 2, result: 'done' }\n```\n\nAs you can see, first effect call was rejected and cancelled, but timeout itself was not cancelled, and printed message.\n\nLet's change code above:\n\n```javascript\nimport { createReEffect, TAKE_LAST } from 'effector-reeffect'\n\nconst reeffect = createReEffect({ strategy: TAKE_LAST })\n\nreeffect.watch(_ =\u003e console.log('reeffect called:', _))\nreeffect.done.watch(_ =\u003e console.log('reeffect done:', _))\nreeffect.fail.watch(_ =\u003e console.log('reeffect fail:', _))\nreeffect.cancelled.watch(_ =\u003e console.log('reeffect cancelled:', _))\n\nreeffect.use((params, onCancel) =\u003e {\n  let timeout\n  onCancel(() =\u003e clearTimeout(timeout))\n  return new Promise(resolve =\u003e {\n    timeout = setTimeout(() =\u003e {\n      console.log(`-\u003e AHA! TIMEOUT FROM EFFECT WITH PARAMS: ${params}`)\n      resolve('done')\n    }, 1000)\n  })\n})\n\nreeffect(1)\nreeffect(2)\n```\n\nNow ReEffect know, how to cancel your Promise's logic, and will do it while cancelling operation:\n\n```\nreeffect called: 1\nreeffect called: 2\nreeffect cancelled: { params: 1,\n  error: Error: Cancelled due to \"TAKE_LAST\", new effect was added }\n-\u003e AHA! TIMEOUT FROM EFFECT WITH PARAMS: 2\nreeffect done: { params: 2, result: 'done' }\n```\n\nThis could be done with any asynchronous operation, which supports cancellation or abortion.\n\n### [axios](https://github.com/axios/axios)\n\nAxios supports cancellation via [_cancel token_](https://github.com/axios/axios#cancellation):\n\n```javascript\nreeffect.use(({ id }, onCancel) =\u003e {\n  const source = CancelToken.source()\n  onCancel(() =\u003e source.cancel())\n  return axios.get(`https://example.com/users/${id}`, {\n    cancelToken: source.token,\n  })\n})\n```\n\n### [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)\n\nFetch API supports cancellation via [_AbortController_](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) ([read more](https://developers.google.com/web/updates/2017/09/abortable-fetch)):\n\n```javascript\nreeffect.use(({ id }, onCancel) =\u003e {\n  const controller = new AbortController()\n  onCancel(() =\u003e controller.abort())\n  return fetch(`https://example.com/users/${id}`, {\n    signal: controller.signal,\n  })\n})\n```\n\n### [ky](https://github.com/sindresorhus/ky)\n\nKy is built on top of Fetch API, and supports cancellation via [_AbortController_](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) as well:\n\n```javascript\nreeffect.use(({ id }, onCancel) =\u003e {\n  const controller = new AbortController()\n  onCancel(() =\u003e controller.abort())\n  return ky(`https://example.com/users/${id}`, {\n    signal: controller.signal,\n  }).json()\n})\n```\n\n### [request](https://github.com/request/request)\n\n_**Note**: request has been [deprecated](https://github.com/request/request/issues/3142), you probably should not use it._\n\nRequest HTTP client supports [`.abort()` method](https://github.com/request/request/issues/772):\n\n```javascript\nreeffect.use(({ id }, onCancel) =\u003e {\n  let r\n  onCancel(() =\u003e r.abort())\n  return new Promise((resolve, reject) =\u003e {\n    r = request(`https://example.com/users/${id}`, (err, resp, body) =\u003e\n      err ? reject(err) : resolve(body)\n    )\n  })\n})\n```\n\n### [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)\n\nIf you happen to use good old `XMLHttpRequest`, I will not blame you (but others definitely will). Good to know it supports cancellation too, via [`.abort()` method](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort):\n\n```javascript\nreeffect.use(({ id }, onCancel) =\u003e {\n  let xhr\n  onCancel(() =\u003e xhr.abort())\n  return new Promise(function (resolve, reject) {\n    xhr = new XMLHttpRequest()\n    xhr.open('GET', `https://example.com/users/${id}`)\n    xhr.onload = function () {\n      if (this.status \u003e= 200 \u0026\u0026 this.status \u003c 300) {\n        resolve(xhr.response)\n      } else {\n        reject({\n          status: this.status,\n          statusText: xhr.statusText,\n        })\n      }\n    }\n    xhr.onerror = function () {\n      reject({\n        status: this.status,\n        statusText: xhr.statusText,\n      })\n    }\n    xhr.send()\n  })\n})\n```\n\n## FAQ\n\n### Can I use ReEffect with Domain?\n\nYes! Alongside with `createReEffect` ReEffect package exports factory `createReEffectFactory`, you can use it to wrap `createEffect` from domain:\n\n```javascript\nimport { createDomain } from 'effector'\nimport { createReEffectFactory } from 'effector-reeffect'\n\nconst domain = createDomain()\nconst createReEffect = createReEffectFactory(domain.createEffect)\nconst fetchUser = createReEffect(/* ... */)\n// -\u003e fetchUser will belong to domain\n```\n\n### Can I use ReEffect with `fork`?\n\nYes, with Effector version 22.\n\n### Can I use ReEffect with `attach`?\n\nI didn't try it, but most probably no :(\u003cbr /\u003e\nFirst of all, after `attach` you will get regular Effect, not ReEffect, and secondarily, looks like `attach` implementation [replaces `req` parameter](https://github.com/zerobias/effector/blob/d2f711c9fc702436e44dcf9637e4e7ee5a884570/src/effector/attach.js#L34), which highly likely will break ReEffect functionality.\u003cbr /\u003e\nThere is [issue #8](https://github.com/yumauri/effector-reeffect/issues/8) to track this case.\n\nIf you want just attach store to your ReEffect, you can try technique, called \"CoEffect\":\n\n```javascript\n/**\n * Creates CoEffect - ReEffect, attached to the store value\n */\nfunction createCoEffect({ store, handler, ...config }) {\n  const fx = createReEffect(config)\n\n  // save original `use`\n  const use = fx.use\n\n  // replace original `use`, to be able to change handler on CoEffect\n  // you can omit this, if you don't intend to replace CoEffect handler\n  fx.use = fn =\u003e (handler = fn)\n\n  // on each store change replace handler for ReEffect,\n  // so it will be called with actual store value every time\n  store.watch(value =\u003e {\n    use((payload, onCancel) =\u003e handler(payload, value, onCancel))\n  })\n\n  return fx\n}\n```\n\n## Sponsored\n\n[\u003cimg src=\"https://setplex.com/img/logo.png\" alt=\"Setplex OTT Platform\" width=\"236\"\u003e](https://setplex.com/en/)\n\n[Setplex OTT Platform](https://setplex.com/en/)\n","funding_links":[],"categories":["Packages"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyumauri%2Feffector-reeffect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyumauri%2Feffector-reeffect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyumauri%2Feffector-reeffect/lists"}