{"id":15638106,"url":"https://github.com/neurosnap/cofx","last_synced_at":"2025-04-14T03:15:39.018Z","repository":{"id":40743412,"uuid":"131670735","full_name":"neurosnap/cofx","owner":"neurosnap","description":"A node and javascript library that helps developers describe side-effects as data in a declarative, flexible API.","archived":false,"fork":false,"pushed_at":"2022-06-25T05:57:57.000Z","size":286,"stargazers_count":94,"open_issues_count":3,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-14T03:15:28.090Z","etag":null,"topics":["asynchronous","cofx","data","javascript","node","promise","side-effects","yield"],"latest_commit_sha":null,"homepage":"","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/neurosnap.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-05-01T03:01:35.000Z","updated_at":"2023-12-25T11:35:48.000Z","dependencies_parsed_at":"2022-08-20T00:41:29.839Z","dependency_job_id":null,"html_url":"https://github.com/neurosnap/cofx","commit_stats":null,"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neurosnap%2Fcofx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neurosnap%2Fcofx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neurosnap%2Fcofx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neurosnap%2Fcofx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/neurosnap","download_url":"https://codeload.github.com/neurosnap/cofx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248813803,"owners_count":21165634,"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":["asynchronous","cofx","data","javascript","node","promise","side-effects","yield"],"created_at":"2024-10-03T11:18:46.688Z","updated_at":"2025-04-14T03:15:38.993Z","avatar_url":"https://github.com/neurosnap.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cofx [![Build Status](https://travis-ci.org/neurosnap/cofx.svg?branch=master)](https://travis-ci.org/neurosnap/cofx)\n\nA node and javascript library that helps developers describe side-effects as data in a declarative, flexible API.\n\n## Features\n\n* Perform non-blocking IO operations synchronously\n* Generators yield data structures, not IO\n* Testability\n* Ability to write custom effect handlers\n\n## Why?\n\n[Blog article I wrote about it](https://erock.io/simplify-testing-async-io-javascript/)\n\nMaintaining side-effects, especially IO, are difficult to write in javascript.  They are asynchronous,\nthey require effort to prevent callback hell, and they can be difficult to test.\n\nThis library leverages the power of generators to help control the flow of side-effects as well as\ncreate a platform to easily test those side-effects.  The key feature of this library is to\ndescribe those side-effects as data.\n\nInstead of a generator activating side-effects (e.g. making HTTP requests)\nit yields data objects that represent how side-effects ought to be executed.\n\nEffectively this makes testing side-effects as easy as checking that each step\nin a generator returns the proper data structure.  Because we are leveraging generators\nthis library also helps reduce the level of nesting when dealing with asynchronous operations.\n\nThis library was inspired by [redux-saga](https://github.com/redux-saga/redux-saga)\nand [re-frame](https://github.com/Day8/re-frame).  Whenever I left the world\nof react/redux and wanted to test my async/await/generator functions it would require\nmocking/intercepting HTTP requests which is a terrible developer experience.\nInstead this library does all the heavy lifting of activating the side-effects while\nthe end-developer can write code in a declarative manner.\n\nThis technique is very popular, the prime example is `react`.  Testing react components\nis easy because the components are functions that accept state and then return data as HTML.\n\n```js\nconst view = f(state);\n```\n\nThe functions themselves do not mutate the DOM, they tell the `react` runtime how to mutate the DOM.\nThis is a critical distinction and pivotal for understanding how this library operates.\nEffectively the end-developer only concerns itself with the shape of the data being returned from their\nreact components and the react runtime does the rest.\n\n\n## References\n\n* [Effects as Data talk by Richard Feldman](https://www.youtube.com/watch?v=6EdXaWfoslc)\n* [use-cofx](https://github.com/neurosnap/use-cofx) react hook for cofx\n* [redux-cofx](https://github.com/neurosnap/redux-cofx) side-effect middleware for redux\n* [gen-tester](https://github.com/neurosnap/gen-tester) test cofx generators\n* [express-cofx-router](https://github.com/neurosnap/express-cofx-router) express router that supports using cofx generators\n* [redux-router-cofx](https://github.com/neurosnap/redux-router-cofx) activate side-effects with `connected-react-router`\n\n## How?\n\n`cofx` will yield to data objects and activate side-effects based on the shape of those objects.\nAn effect object looks something like this:\n\n```json\n{\n  \"type\": \"CALL\",\n  \"fn\": [function],\n  \"args\": [\"list\", \"of\", \"arguments\"]\n}\n```\n\nThis JSON object tells the `cofx` runtime that it should call a function, `fn` with arguments\nin `args`.  From there the runtime knows exactly what to do with the results of the function, whether it's\na promise or a not.\n\n`task` returns a promise that receives the return value of the generator.\n\n```js\nimport { call, task } from 'cofx';\n\n// running this generator without the runtime `task` would simply return data objects that describe\n// what the runtime should do.  Really it returns a DSL that the runtime interprets.\nfunction* fetchBin() {\n  const resp = yield call(fetch, 'http://httpbin.org/get');\n  // sending an array makes `call` activate the function `json` on `resp` object\n  // this is required because of the way fetch uses context to determine if the Body\n  // promise has been used already.\n  const data = yield call([resp, 'json']);\n  return { ...data, extra: 'stuff' };\n}\n\n// `task` is in charge of handling the actual side-effects\n// all user code does not contain any side-effects because they return json objects\ntask(fetchBin)\n  .then(console.log)\n  .catch(console.error);\n```\n\n## Cancelling a task (added v2.0)\n\nCancelling a task is possible by passing a promise into the `task` that once resolved, will\ncancel the task.\n\n```js\nimport { task, delay } from 'cofx';\n\nfunction* waiting(duration) {\n  try {\n    yield delay(duration);\n  } catch (err) {\n    console.log(err);\n  }\n}\n\nconst cancel = new Promise((resolve) =\u003e {\n  setTimeout(() =\u003e {\n    resolve('cancel the task!');\n  }, 500);\n});\n\ntask({ fn: waiting, args: [1000], cancel })\n  .then(console.log)\n  .catch(console.error);\n```\n\nCheck out the API section for more effects.\n\n## Testing\n\nTaking the previous example, this is how you would test it:\n\n```js\nconst test = require('tape');\n\ntest('test fetchBin', (t) =\u003e {\n  const gen = fetchBin();\n\n  t.deepEqual(\n    gen.next().value,\n    call(fetch, 'http://httpbin.org/get'),\n    'should make http request',\n  );\n\n  const respValue = { resp: 'value', json: 'hi' };\n  t.deepEqual(\n    gen.next(respValue).value,\n    call([respValue, 'json']),\n    'should get json from response',\n  );\n\n  const last = gen.next({ data: 'value' });\n  t.ok(last.done, 'generator should finish');\n  t.deepEqual(\n    last.value,\n    { data: 'value', extra: 'stuff' },\n    'should return data',\n  );\n});\n```\n\nUsing a little helper library called [gen-tester](https://github.com/neurosnap/gen-tester)\nwe can make this even easier.\n\n```js\nconst { genTester, yields } = require('gen-tester');\n\ntest('test fetchBin', (t) =\u003e {\n  t.plan(1);\n\n  const respValue = { resp: 'value', json: 'hi' };\n  const returnValue = { data: 'value', extra: 'stuff' };\n\n  const tester = genTester(genCall);\n  const { actual, expect } = tester(\n    yields(\n      call(fetch, 'http://httpbin.org/get'),\n      respValue, // the result value of `resp` in the generator\n    ),\n    yields(\n      call([respValue, 'json']),\n      { data: 'value' }, // the result value of `data` in the generator\n    ),\n    returnValue,\n  );\n\n  t.deepEqual(actual, expected);\n});\n```\n\nWhen the generator function does not get called by `task`\nall it does is return JSON at every `yield`.  This is the brilliance of describing\nside-effects as data: we can test our generator function synchronously, without\nneeding any HTTP interceptors or mocking functions!  So even though at every yield\nthis library will make asynchronous calls, for testing, we can step through the\ngenerator one step after another and make sure the yield makes the correct call.\n\n## API\n\n### task\n\nManages async flow for a generator.  This is an alias to the `co` function.\n\n### call\n\n```js\nconst { task, call } = require('cofx');\nconst fetch = require('node-fetch');\n\nfunction* example() {\n  yield call(fetch, 'http://google.com')\n  return 'hi';\n}\n\ntask(example);\n```\n\n### all\n\nUses `Promise.all` to execute effects in parallel.  Could be an array of effects\nor an object of effects.\n\n```js\nconst { task, call, all } = require('cofx');\nconst fetch = require('node-fetch');\n\nfunction* example() {\n  const resp = yield all([\n    call(fetch, 'http://google.com'),\n    call(fetch, 'http://something-else.com'),\n  ]);\n  const data = yield all(resp.map((r) =\u003e call([r, 'json'])));\n  return data;\n}\n\ntask(example);\n```\n\n### race\n\nUses `Promise.race` to execute effects in parallel.  Could be an array of effects\nor an object of effects.  The request that finishes first will be returned.\n\n```js\nconst { task, call, race } = require('cofx');\nconst fetch = require('node-fetch');\n\nfunction* example() {\n  const resp = yield race([\n    call(fetch, 'http://google.com'),\n    call(fetch, 'http://something-really-slow.com'),\n  ]);\n  const data = yield call([resp, 'json']); // resp is the result of the fastest request\n  return data;\n}\n\ntask(example);\n```\n\n```js\nconst { task, call, race, delay } = require('cofx');\nconst fetch = require('node-fetch');\n\nfunction* example() {\n  const resp = yield race({\n    google: call(fetch, 'http://google.com'),\n    delay: delay(10 * 1000),\n  });\n\n  console.log(resp);\n  // -\u003e { google: { ... }, delay: undefined }\n\n  if (resp.google) {\n    const data = yield call([resp, 'json']); // resp is the result of the fastest request\n    return data;\n  }\n}\n\ntask(example);\n```\n\n### fork (added v2.0)\n\nForks (attached) an effect without the generator waiting for that effect to finish.  If\na task gets cancelled, this fork will also be cancelled.\n\n```js\nconst { task, fork } = require('cofx');\nconst fetch = require('node-fetch');\n\nfunction effect() {\n  return new Promise((resolve) =\u003e {\n    setTimeout(() =\u003e {\n      resolve();\n      console.log('ACTIVATE');\n    }, 5000);\n  });\n}\n\nfunction* example() {\n  yield fork(effect);\n  console.log('COOL');\n}\n\ntask(example);\n// COOL\n// ... five seconds later\n// ACTIVATE\n```\n\n### spawn\n\nSpawns (dettached) an effect without the generator waiting for that effect to finish.  If\na task gets cancelled, this spawn will *NOT* be cancelled.\n\n```js\nconst { task, spawn } = require('cofx');\nconst fetch = require('node-fetch');\n\nfunction effect() {\n  return new Promise((resolve) =\u003e {\n    setTimeout(() =\u003e {\n      resolve();\n      console.log('ACTIVATE');\n    }, 5000);\n  });\n}\n\nfunction* example() {\n  yield spawn(effect);\n  console.log('COOL');\n}\n\ntask(example);\n// COOL\n// ... five seconds later\n// ACTIVATE\n```\n\n### delay\n\nThis will `sleep` the generator for the designated amount of time.\n\n```js\nconst { task, delay } = require('cofx');\n\nfunction* example() {\n  console.log('INIT')\n  yield delay(1000);\n  console.log('END');\n}\n\ntask(example);\n// INIT\n// ... one second later\n// END\n```\n\n### factory\n\nThis is what creates `task`.  This allows end-developers to build their own\neffect middleware.  When using middleware it must return a promise, something that\n`co` understands how to handle, and to allow other middleware to handle the effect\nas well, you must return `next(effect)`;\n\n#### error effect handler\n\n```js\nconst { factory } = require('cofx');\n\nconst ERROR = 'ERROR';\nconst error = (msg) =\u003e ({ type: ERROR, msg });\nconst middleware = (next) =\u003e (effect) =\u003e {\n  if (effect.type === ERROR) {\n    return Promise.reject(effect.msg);\n  }\n\n  return next(effect);\n};\n\nfunction* example() {\n  yield error('SOMETHING HAPPENED');\n}\n\nconst customTask = factory(middleware);\ncustomTask(example).catch((err) =\u003e {\n  console.log(`ERROR: ${err}`);\n});\n\n// ERROR: SOMETHING HAPPENED\n```\n\n#### date effect handler\n\n```js\nconst { factory } = require('cofx');\n\nconst GET_DATE = 'GET_DATE';\nconst getDate = () =\u003e ({ type: GET_DATE });\nconst middleware = (next) =\u003e (effect) =\u003e {\n  if (effect.type === GET_DATE) {\n    return Promise.resolve(new Date());\n  }\n\n  return next(effect);\n};\n\nfunction* example() {\n  const now = yield getDate();\n  return now;\n}\n\nconst customTask = factory(middleware);\ncustomTask(example).then((now) =\u003e {\n  console.log(now);\n});\n\n// Fri Sep 21 2018 13:44:24 GMT-0400 (Eastern Daylight Time)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fneurosnap%2Fcofx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fneurosnap%2Fcofx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fneurosnap%2Fcofx/lists"}