{"id":18552478,"url":"https://github.com/andrejewski/affection","last_synced_at":"2025-10-07T06:02:38.354Z","repository":{"id":57174011,"uuid":"150917479","full_name":"andrejewski/affection","owner":"andrejewski","description":"Wish I had some..declarative side-effects","archived":false,"fork":false,"pushed_at":"2020-05-11T17:15:41.000Z","size":63,"stargazers_count":40,"open_issues_count":4,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-05T00:02:37.825Z","etag":null,"topics":["data-driven","declarative","side-effects"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/andrejewski.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":"2018-09-30T01:27:55.000Z","updated_at":"2024-01-12T15:02:52.000Z","dependencies_parsed_at":"2022-09-02T11:21:32.525Z","dependency_job_id":null,"html_url":"https://github.com/andrejewski/affection","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Faffection","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Faffection/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Faffection/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Faffection/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andrejewski","download_url":"https://codeload.github.com/andrejewski/affection/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248123773,"owners_count":21051531,"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":["data-driven","declarative","side-effects"],"created_at":"2024-11-06T21:14:20.336Z","updated_at":"2025-10-07T06:02:33.334Z","avatar_url":"https://github.com/andrejewski.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Affection\n\u003e Declarative side-effects\n\n```sh\nnpm install affection\n```\n\n[![npm](https://img.shields.io/npm/v/affection.svg)](https://www.npmjs.com/package/affection)\n[![Build Status](https://travis-ci.org/andrejewski/affection.svg?branch=master)](https://travis-ci.org/andrejewski/affection)\n[![Greenkeeper badge](https://badges.greenkeeper.io/andrejewski/affection.svg)](https://greenkeeper.io/)\n\nAffection is a library for describing side-effects as plain data and providing composition utilities.\nThis project aims to improve on similar libraries by not using generators.\n\nGenerators make testing difficult in that:\n\n- They can have internal state.\n- Each segment of the function cannot be tested in isolation.\n- Each segment of the function can only be reached after the segments before it.\n- Generators are awkward. Conversing with a generator using `next()` isn't as simple as function calling.\n- Composition of generators is harder than functions inherently.\n\nSo Affection is all about functions, with the goals:\n\n- Improve testability through the use of pure functions.\n- Improve code reuse through la-a-carte composition of side-effects.\n\nLet's see how we do.\n\n## Examples\n\nThis first example does not use any composition.\n\n```js\nimport { run, call, callMethod } from 'affection'\n\nconst getJSON = url =\u003e [\n  call(fetch, [url]),\n  resp =\u003e [callMethod(resp, 'json')]\n]\n\nasync function main () {\n  const payload = await run(getJSON('http://example.com'))\n  console.log(payload)\n}\n```\n\nThis second example does the same as the first.\nHere we are using the composition utilities.\n\n```js\nimport { step, runStep, batchSteps, call, callMethod } from 'affection'\n\nconst fetchUrl = url =\u003e call(fetch, [url])\nconst readJSON = resp =\u003e callMethod(resp, 'json')\nconst getJSON = batchSteps([fetchUrl, readJSON].map(step))\n\nasync function main () {\n  const payload = await runStep(getJSON, 'http://example.com')\n  console.log(payload)\n}\n```\n\n## Documentation\nThe package contains the following:\n\n##### Effects\n- [`call(func, args, context)`](#call)\n- [`callMethod(obj, method, args)`](#callmethod)\n- [`all(effects)`](#all)\n- [`race(effects)`](#race)\n- [`itself(value)`](#itself)\n\nSee [`defaultHandle`](#defaulthandle) for adding more.\n\n##### Execution\n- [`run(plan[, handle])`](#run)\n\n##### Composition\n- [`step(makeEffect)`](#step)\n- [`mapStep(step, transform)`](#mapstep)\n- [`batchStep(steps)`](#batchsteps)\n- [`runStep(step, input[, handle])`](#runstep)\n\n### `call`\n\u003e `call(func: function, args: Array\u003cany\u003e, context: any): Effect`\n\nDescribes a function call of `func.apply(context, args)`.\n\n### `callMethod`\n\u003e `callMethod(obj: any, method: String, args: Array\u003cany\u003e): Effect`\n\nDescribes a method call of `obj[method].apply(obj, args)`\n\n### `all`\n\u003e `all(effects: Array\u003cEffect\u003e): Effect`\n\nDescribes combining effects. Like `Promise.all`.\n\n### `race`\n\u003e `race(effects: Array\u003cEffect\u003e): Effect`\n\nDescribes racing effects. Like `Promise.race`.\n\n### `itself`\n\u003e `itself(value: any): Effect`\n\nDescribes a value. This is an identity function for Effects.\n\n### `defaultHandle`\n\u003e `defaultHandle(effect: Effect, handle: function): any`\n\nPerforms the action described by a particular effect.\n`defaultHandle` provides the handling for the effects included in Affection.\nTo add more, create a new handle that wraps `defaultHandle` and pass that to `run`.\n\nFor example, say we want to add a timeout effect:\n\n```js\nimport { defaultHandle } from 'affection'\n\nexport function timeout (duration) {\n  return { type: 'timeout', duration }\n}\n\nexport function myHandle (effect, handle) {\n  if (effect.type === 'timeout') {\n    return new Promise(resolve =\u003e setTimeout(resolve, effect.duration))\n  }\n  return defaultHandle(effect, handle)\n}\n\n// Later...\n\nasync function main () {\n  await run([timeout(1000)], myHandler)\n  // Will have waited a second\n}\n```\n\n### `run`\n\u003e `run(plan: [Effect, function?], handle: function = defaultHandle): any`\n\nExecutes a plan.\nA plan is an array where the first element is an Effect to be handled using `handle` and the second element is a function to call with the result of the Effect.\nIf the function is not provided, execution terminates and the result is returned.\n\n### `step`\n\u003e `step(makeEffect: any -\u003e Effect): Step`\n\nCreates a step.\nA step is a means of encapsulating an effect without needing a plan (as described by `run`).\n\nThis is hard to understand without an understanding of how `run` works.\nThe `run` function is recursively executing plans until there is nothing more to do.\nA step is a way of saying, \"Execute this effect; I don't know what happens with the result.\"\nThis is for code reuse: effects should be decoupled from their consumers.\n\nFor more clarity, let's look at the `step` function:\n\n```js\nconst step = makeEffect =\u003e next =\u003e input =\u003e [makeEffect(input), next]\n```\n\nWe define our `makeEffect` without needing to know the consumer.\nThe `next` is what will consume the result.\nLater, steps are composed when the consumers are known.\nFinally, the step is given an `input` to build its effect.\n\nIn summary, steps decouple:\n- Creating the effect: `makeEffect`\n- Consuming the effect's result: `next`\n- Building the final plan, given an `input`\n\n### `mapStep`\n\u003e `mapStep(step: Step, transform: function): Step`\n\nCreates a new step which will return the result of `transform` called with the input to the `step` `makeEffect` and the result of the Effect.\n\nThis is good for passing along context without mucking up simple steps.\nFor example, we are building a dictionary of the most used word for each country.\nWe want to retain the country we are querying about in the result.\n\n```js\nconst getMostUsedWordInCountry = country =\u003e call(MyAPI, [country])\nconst countryWordStep = step(getMostUsedWordInCountry)\nconst getCountryWord = mapStep(countryWordStep, (result, country) =\u003e ({ country, word: result }))\n\nrunStep(getCountryWord, 'Canada').then(result =\u003e {\n  console.log(result)\n  // =\u003e { country: 'Canada', word: 'Sorry' }\n})\n```\n\n### `batchSteps`\n\u003e `batchSteps(steps: Array\u003cStep\u003e): Step`\n\nCreates a new step which will call each step passing the result of first step to the next and so on.\n\n### `runStep`\n\u003e `runStep(step: Step, input: any, handle: function = defaultHandle): any`\n\nExecutes a `step` with a given `input`.\nUses `run` so `handle` works in the same way.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrejewski%2Faffection","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandrejewski%2Faffection","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrejewski%2Faffection/lists"}