{"id":15498199,"url":"https://github.com/getify/fasy","last_synced_at":"2025-04-04T09:06:42.431Z","repository":{"id":46330734,"uuid":"106551787","full_name":"getify/fasy","owner":"getify","description":"FP iterators that are both eager and asynchronous","archived":false,"fork":false,"pushed_at":"2022-08-27T14:46:02.000Z","size":140,"stargazers_count":547,"open_issues_count":5,"forks_count":19,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-03-28T08:07:36.273Z","etag":null,"topics":["async","asynchronous-programming","fp","functional-programming","javascript","library"],"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/getify.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["getify"],"patreon":"getify","custom":["https://www.paypal.com/paypalme2/getify","https://www.blockchain.com/btc/address/32R5dVrqirdcbiyvUw85y7YbPFZTd7YpnH"]}},"created_at":"2017-10-11T12:33:38.000Z","updated_at":"2025-03-27T23:45:46.000Z","dependencies_parsed_at":"2022-09-19T14:43:12.245Z","dependency_job_id":null,"html_url":"https://github.com/getify/fasy","commit_stats":null,"previous_names":[],"tags_count":33,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getify%2Ffasy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getify%2Ffasy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getify%2Ffasy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getify%2Ffasy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/getify","download_url":"https://codeload.github.com/getify/fasy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247149500,"owners_count":20891954,"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":["async","asynchronous-programming","fp","functional-programming","javascript","library"],"created_at":"2024-10-02T08:42:20.961Z","updated_at":"2025-04-04T09:06:42.412Z","avatar_url":"https://github.com/getify.png","language":"JavaScript","readme":"# Fasy\n\n[![Build Status](https://travis-ci.org/getify/fasy.svg?branch=master)](https://travis-ci.org/getify/fasy)\n[![npm Module](https://badge.fury.io/js/fasy.svg)](https://www.npmjs.org/package/fasy)\n[![Coverage Status](https://coveralls.io/repos/github/getify/fasy/badge.svg?branch=master)](https://coveralls.io/github/getify/fasy?branch=master)\n[![Modules](https://img.shields.io/badge/modules-ESM%2BUMD%2BCJS-a1356a)](https://nodejs.org/api/packages.html#dual-commonjses-module-packages)\n[![License](https://img.shields.io/badge/license-MIT-a1356a)](LICENSE.txt)\n\n**Fasy** (/ˈfāsē/) is a utility library of FP array iteration helpers (like `map(..)`, `filter(..)`, etc), as well as function composition and transducing.\n\nWhat's different from other FP libraries is that its methods are capable of operating asynchronously, via `async function` functions and/or `function*` generators. **Fasy** supports both concurrent and serial asynchrony.\n\nFor concurrent asynchrony, **Fasy** also supports limiting the batch size to avoid overloading resources.\n\n## Environment Support\n\nThis library uses ES2017 (and ES6) features. If you need to support environments prior to ES2017, transpile it first (with Babel, etc).\n\n## At A Glance\n\nHere's a quick example:\n\n```js\nvar users = [ \"bzmau\", \"getify\", \"frankz\" ];\n\nFA.concurrent.map( getOrders, users )\n.then( userOrders =\u003e console.log( userOrders ) );\n```\n\nThis would work fine with any implementation of `map(..)` if `getOrders(..)` was synchronous. But `concurrent.map(..)` is different in that it handles/expects asynchronously completing functions, like `async function` functions or `function*` generators. Of course, you can *also* use normal synchronous functions as well.\n\n`concurrent.map(..)` will run each call to `getOrders(..)` concurrently (aka \"in parallel\"), and once all are complete, fulfill its returned promise with the final result of the mapping.\n\nBut what if you wanted to run each `getOrders(..)` call one at a time, in succession? Use `serial.map(..)`:\n\n```js\nvar users = [ \"bzmau\", \"getify\", \"frankz\" ];\n\nFA.serial.map( getOrders, users )\n.then( userOrders =\u003e console.log( userOrders ) );\n```\n\nAs with `concurrent.map(..)`, once all mappings are complete, the returned promise is fulfilled with the final result of the mapping.\n\n**Fasy** handles `function*` generators via its own [generator-runner](https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch4.md#promise-aware-generator-runner), similar to utilities provided by various async libraries (e.g., [`asynquence#runner(..)`](https://github.com/getify/asynquence/tree/master/contrib#runner-plugin), [`Q.spawn(..)`](https://github.com/kriskowal/q/wiki/API-Reference#qspawngeneratorfunction)).:\n\n```js\nvar users = [ \"bzmau\", \"getify\", \"frankz\" ];\n\nFA.serial.map(\n    function *getOrders(username){\n        var user = yield lookupUser( username );\n        return lookupOrders( user.id );\n    },\n    users\n)\n.then( userOrders =\u003e console.log( userOrders ) );\n```\n\n## Background/Motivation\n\nFunctional helpers like `map(..)` / `filter(..)` / `reduce(..)` are quite handy for iterating through a list of operations:\n\n```js\n[1,2,3,4,5].filter(v =\u003e v % 2 == 0);\n// [2,4]\n```\n\nThe [sync-async pattern](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch4.md#generators--promises) of `async function` functions offers much more readable asynchronous flow control code:\n\n```js\nasync function getOrders(username) {\n    var user = await lookupUser( username );\n    return lookupOrders( user.id );\n}\n\ngetOrders( \"getify\" )\n.then( orders =\u003e console.log( orders ) );\n```\n\nAlternately, you could use a `function*` generator along with a [generator-runner](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch4.md#promise-aware-generator-runner) (named `run(..)` in the below snippet):\n\n```js\nrun( function *getOrders(username){\n    var user = yield lookupUser( username );\n    return lookupOrders( user.id );\n}, \"getify\" )\n.then( orders =\u003e console.log( orders ) );\n```\n\nThe problem is, mixing FP-style iteration like `map(..)` with `async function` functions / `function*` generators doesn't quite work:\n\n```js\n// BROKEN CODE -- DON'T COPY!!\n\nasync function getAllOrders() {\n    var users = [ \"bzmau\", \"getify\", \"frankz\" ];\n\n    var userOrders = users.map( function getOrders(username){\n        // `await` won't work here inside this inner function\n        var user = await lookupUser( username );\n        return lookupOrders( user.id );\n    } );\n\n    // everything is messed up now, since `map(..)` works synchronously\n    console.log( userOrders );\n}\n```\n\nThe `await` isn't valid inside the inner function `getOrders(..)` since that's a normal function, not an `async function` function. Also, `map(..)` here is the standard array method that operates synchronously, so it doesn't wait for all the lookups to finish.\n\nIf it's OK to run the `getOrders(..)` calls concurrently -- in this particular example, it quite possibly is -- then you could use `Promise.all(..)` along with an inner `async function` function:\n\n```js\nasync function getAllOrders() {\n    var users = [ \"bzmau\", \"getify\", \"frankz\" ];\n\n    var userOrders = await Promise.all( users.map( async function getOrders(username){\n        var user = await lookupUser( username );\n        return lookupOrders( user.id );\n    } ) );\n\n    // this works\n    console.log( userOrders );\n}\n```\n\nUnfortunately, aside from being more verbose, this \"fix\" is fairly limited. It really only works for `map(..)` and not for something like `filter(..)`. Also, as that fix assumes concurrency, there's no good way to do the FP-style iterations serially.\n\n## Overview\n\nWith **Fasy**, you can do either concurrent or serial iterations of asynchronous operations.\n\n### Concurrent Asynchrony\n\nFor example, consider this [`concurrent.map(..)`](docs/concurrent-API.md#concurrentmap) operation:\n\n```js\nasync function getAllOrders() {\n    var users = [ \"bzmau\", \"getify\", \"frankz\" ];\n\n    var userOrders = await FA.concurrent.map(\n        async function getOrders(username){\n            var user = await lookupUser( username );\n            return lookupOrders( user.id );\n        },\n        users\n    );\n\n    console.log( userOrders );\n}\n```\n\nNow let's look at the same task, but with a [`serial.map(..)`](docs/serial-API.md#serialmap) operation:\n\n```js\nasync function getAllOrders() {\n    var users = [ \"bzmau\", \"getify\", \"frankz\" ];\n\n    var userOrders = await FA.serial.map(\n        async function getOrders(username){\n            var user = await lookupUser( username );\n            return lookupOrders( user.id );\n        },\n        users\n    );\n\n    console.log( userOrders );\n}\n```\n\nLet's look at a `filter(..)` example:\n\n```js\nasync function getActiveUsers() {\n    var users = [ \"bzmau\", \"getify\", \"frankz\" ];\n\n    return FA.concurrent.filter(\n        async function userIsActive(username){\n            var user = await lookupUser( username );\n            return user.isActive;\n        },\n        users\n    );\n}\n```\n\nThe equivalent of this would be much more verbose/awkward than just a simple `Promise.all(..)` \"fix\" as described earlier. And of course, you can also use [`serial.filter(..)`](docs/serial-API.md#serialfilter) to process the operations serially if necessary.\n\n#### Limiting Concurrency\n\nTo limit the concurrency (aka, parallelism) of your operations, there are two modes to select from: *continuous pooling* (default) and *batch*.\n\n**Note:** Such limitations on concurrency are often useful when the operations involve finite system resources, like OS file handles or network connection ports, and as such you want to avoid exhausting those resources and creating errors or over-burdening the system.\n\nTo illustrate, *continuous pooling* mode:\n\n```js\nasync function getAllURLs(urls) {\n    var responses = await FA.concurrent(5).map(fetch,urls);\n\n    // .. render responses\n}\n```\n\nIn this example, the `(5)` part of `FA.concurrent(5)` limits the concurrency to only (up to) five active `fetch(..)` calls at any given moment. As soon as one finishes, if there are any more calls waiting, the next one is activated. This argument must be greater than zero.\n\nThe `concurrent(5)` call is actually a shorthand for `concurrent(5,5)`, which includes a second argument: minimum active threshold. In other words, the way *continuous pooling* mode works is, the first five `fetch(..)` calls are activated, and when the first one finishes, the active count is now down to `4`, which is below that specified `5` threshold, so the next one (if any are waiting) is activated.\n\nIn contrast to *continuous pooling* mode, *batch* mode is activated by explicitly specifying a number for this second argument that is lower than the first argument (but still greater than zero).\n\nFor example, `concurrent(5,1)` runs a batch of five concurrent `fetch(..)` calls, but doesn't start the next batch of calls until the active count falls below `1` (aka, the whole batch finishes):\n\n```js\nasync function getAllURLs(urls) {\n    var responses = await FA.concurrent(5,1).map(fetch,urls);\n\n    // .. render responses\n}\n```\n\nAnd `concurrent(5,3)` would run a batch of five active calls, then refill the active batch set (to five) once the active count gets below `3`.\n\nWith these two limit arguments, you have complete control to fine tune how much concurrent activity is appropriate.\n\nYou can safely call `concurrent(..)` multiple times with the same arguments -- the resulting concurrency-limited API is internally cached -- or with any different arguments, as necessary. You can also store the concurrency-limited API object and re-use it, if you prefer:\n\n```js\nFA.concurrent(5).map(..);\nFA.concurrent(5).filter(..);\nFA.concurrent(12).forEach(..);\n\nvar FAc5 = FA.concurrent(5);\nFAc5.map(..);\nFAc5.filter(..);\n```\n\n### Serial Asynchrony\n\nSome operations are naturally serial. For example, `reduce(..)` wouldn't make any sense processing as concurrent operations; it naturally runs left-to-right through the list. As such, `concurrent.reduce(..)` / `concurrent.reduceRight(..)` delegate respectively to `serial.reduce(..)` / `serial.reduceRight(..)`.\n\nFor example, consider modeling an asynchronous function composition as a serial `reduce(..)`:\n\n```js\n// `prop(..)` is a standard curried FP helper for extracting a property from an object\nvar prop = p =\u003e o =\u003e o[p];\n\n// ***************************\n\nasync function getOrders(username) {\n    return FA.serial.reduce(\n        async (ret,fn) =\u003e fn( ret ),\n        username,\n        [ lookupUser, prop( \"id\" ), lookupOrders ]\n    );\n}\n\ngetOrders( \"getify\" )\n.then( orders =\u003e console.log( orders ) );\n```\n\n**Note:** In this composition, the second call (from `prop(\"id\")` -- a standard FP helper) is **synchronous**, while the first and third calls are **asynchronous**. That's OK, because promises automatically lift non-promise values. [More on that](#syncasync-normalization) below.\n\nThe async composition being shown here is only for illustration purposes. **Fasy** provides [`serial.compose(..)`](docs/serial-API.md#serialcompose) and [`serial.pipe(..)`](docs/serial-API.md#serialpipe) for performing async compositions ([see below](#async-composition)); these methods should be preferred over doing it manually yourself.\n\nBy the way, instead of `async (ret,fn) =\u003e fn(ret)` as the reducer, you can provide a `function*` generator and it works the same:\n\n```js\nasync function getOrders(username) {\n    return FA.serial.reduce(\n        function *composer(ret,fn) { return fn( ret ); },\n        username,\n        [ lookupUser, prop( \"id\" ), lookupOrders ]\n    );\n}\n\ngetOrders( \"getify\" )\n.then( orders =\u003e console.log( orders ) );\n```\n\nSpecifying the reducer as an `async function` function or a `function*` generator gives you the flexibility to do inner `await` / `yield` flow control as necessary.\n\n### Sync/Async Normalization\n\nIn this specific running example, there's no inner asynchronous flow control necessary in the reducer, so it can actually just be a regular function:\n\n```js\nasync function getOrders(username) {\n    return FA.serial.reduce(\n        (ret,fn) =\u003e fn( ret ),\n        username,\n        [ lookupUser, prop( \"id\" ), lookupOrders ]\n    );\n}\n\ngetOrders( \"getify\" )\n.then( orders =\u003e console.log( orders ) );\n```\n\nThere's an important principle illustrated here that many developers don't realize.\n\nA regular function that returns a promise has the same external behavioral interface as an `async function` function. From the external perspective, when you call a function and get back a promise, it doesn't matter if the function manually created and returned that promise, or whether that promise came automatically from the `async function` invocation. In both cases, you get back a promise, and you wait on it before moving on. The *interface* is the same.\n\nIn the first step of this example's reduction, the `fn(ret)` call is effectively `lookupUser(username)`, which is returning a promise. What's different between `serial.reduce(..)` and a standard synchronous implementation of `reduce(..)` as provided by various other FP libraries, is that if `serial.reduce(..)` receives back a promise from a reducer call, it pauses to wait for that promise to resolve.\n\nBut what about the second step of the reduction, where `fn(ret)` is effectively `prop(\"id\")(user)`? The return from *that* call is an immediate value (the user's ID), not a promise (future value).\n\n**Fasy** uses promises internally to normalize both immediate and future values, so the iteration behavior is consistent regardless.\n\n### Async Composition\n\nIn addition to traditional iterations like `map(..)` and `filter(..)`, **Fasy** also supports serial-async composition, which is really just a serial-async reduction under the covers.\n\nConsider:\n\n```js\nasync function getFileContents(filename) {\n    var fileHandle = await fileOpen( filename );\n    return fileRead( fileHandle );\n}\n```\n\nThat is fine, but it can also be recognized as an async composition. We can use [`serial.pipe(..)`](docs/serial-API.md#serialpipe) to define it in point-free style:\n\n```js\nvar getFileContents = FA.serial.pipe( [\n    fileOpen,\n    fileRead\n] );\n```\n\nFP libraries traditionally provide synchronous composition with `pipe(..)` and `compose(..)` (sometimes referred to by other names, like `flow(..)` and `flowRight(..)`, respectively). But asynchronous composition can be quite helpful!\n\n### Async Transducing\n\nTransducing is another flavor of FP iteration; it's a combination of composition and list/data-structure reduction. Multiple `map(..)` and `filter(..)` calls can be composed by transforming them as reducers. Again, many FP libraries support traditional synchronous transducing, but since **Fasy** has serial-async reduction, you can do serial-async transducing as well!\n\nConsider:\n\n```js\nasync function getFileContents(filename) {\n    var exists = await fileExists( filename );\n    if (exists) {\n        var fileHandle = await fileOpen( filename );\n        return fileRead( fileHandle );\n    }\n}\n```\n\nWe could instead model these operations FP-style as a `filter(..)` followed by two `map(..)`s:\n\n```js\nasync function getFileContents(filename) {\n    return FA.serial.map(\n        fileRead,\n        FA.serial.map(\n            fileOpen,\n            FA.serial.filter(\n                fileExists,\n                [ filename ]\n            )\n        )\n    );\n}\n```\n\nNot only is this a bit more verbose, but if we later wanted to be able to get/combine contents from many files, we'd be iterating over a list three times (once each for the `filter(..)` and two `map(..)` calls). That extra iteration is not just a penalty in terms of more CPU cycles, but it also creates an intermediate array in between each step, which is then thrown away, so memory churn becomes a concern.\n\nThis is where transducing shines! If we transform the `filter(..)` and `map(..)` calls into a composition-compatible form (reducers), we can then combine them into one reducer; that means we can do all the steps at once! So, we'll only have to iterate through the list once, and we won't need to create and throw away any intermediate arrays.\n\nWhile this obviously can work for any number of values in a list, we'll keep our running example simple and just process one file:\n\n```js\nasync function getFileContents(filename) {\n    var transducer = FA.serial.compose( [\n        FA.transducers.filter( fileExists ),\n        FA.transducers.map( fileOpen ),\n        FA.transducers.map( fileRead )\n    ] );\n\n    return FA.transducers.into(\n        transducer,\n        \"\", // empty string as initial value\n        [ filename ]\n    );\n}\n```\n\n**Note:** For simplicity, we used the [`transducers.into(..)`](docs/transducers-API.md#transducersinto) convenience method, but the same task could also have used the more general [`transducers.transduce(..)`](docs/transducers-API.md#transducerstransduce) method.\n\n## npm Package\n\nTo install this package from `npm`:\n\n```\nnpm install fasy\n```\n\nAnd to require it in a node script:\n\n```js\nvar FA = require(\"fasy\");\n```\n\nYou can also require any of the three sub-namespaces of this library directly:\n\n```js\n// like this:\nvar concurrent = require(\"fasy/concurrent\");\n\n// or like this:\nvar { serial } = require(\"fasy\");\n```\n\nAs of version 9.0.0, the package (and its sub-namespaces) are also available as ES Modules, and can be imported as so:\n\n```js\nimport FA from \"fasy\";\n\n// or:\n\nimport concurrent from \"fasy/concurrent\";\n\n// or:\n\nimport { serial } from \"fasy\";\n```\n\n**Note:** Starting in version 8.x, **Fasy** was also available in ESM format, but required an ESM import specifier segment `/esm` in **Fasy** `import` paths. This has been deprecated as of version 9.0.0 (and will eventually be removed), in favor of unified import specifier paths via [Node Conditional Exports](https://nodejs.org/api/packages.html#packages_conditional_exports). For ESM `import` statements, always use the specifier style `\"fasy\"` or `\"fasy/concurrent\"`, instead of `\"fasy/esm\"` and `\"fasy/esm/concurrent\"`, respectively.\n\n## API Documentation\n\n* See [Concurrent API](docs/concurrent-API.md) for documentation on the methods in the `FA.concurrent.*` namespace.\n* See [Serial API](docs/serial-API.md) for documenation on the methods in the `FA.serial.*` namespace.\n* See [Transducers API](docs/transducers-API.md) for documentation on the methods in the `FA.transducers.*` namespace.\n\n## Builds\n\n[![Build Status](https://travis-ci.org/getify/fasy.svg?branch=master)](https://travis-ci.org/getify/fasy)\n[![npm Module](https://badge.fury.io/js/fasy.svg)](https://www.npmjs.org/package/fasy)\n[![Modules](https://img.shields.io/badge/modules-ESM%2BUMD%2BCJS-a1356a)](https://nodejs.org/api/packages.html#dual-commonjses-module-packages)\n\nThe distribution library files (`dist/*`) come pre-built with the npm package distribution, so you shouldn't need to rebuild them under normal circumstances.\n\nHowever, if you download this repository via Git:\n\n1. The included build utility (`scripts/build-core.js`) builds (and minifies) `dist/*` files (both UMD and ESM formats) from source.\n\n2. To install the build and test dependencies, run `npm install` from the project root directory.\n\n3. To manually run the build utility with npm:\n\n    ```\n    npm run build\n    ```\n\n4. To run the build utility directly without npm:\n\n    ```\n    node scripts/build-core.js\n    ```\n\n## Tests\n\nA test suite is included in this repository, as well as the npm package distribution. The default test behavior runs the test suite using the files in `src/`.\n\n1. The tests are run with QUnit.\n\n2. You can run the tests in a browser by opening up `tests/index.html`.\n\n3. To run the test utility with npm:\n\n    ```\n    npm test\n    ```\n\n    Other npm test scripts:\n\n    * `npm run test:dist` will run the test suite against `dist/umd/bundle.js` instead of the default of `src/*` files.\n\n    * `npm run test:package` will run the test suite as if the package had just been installed via npm. This ensures `package.json`:`main` properly references the correct file for inclusion.\n\n    * `npm run test:all` will run all three modes of the test suite.\n\n4. To run the test utility directly without npm:\n\n    ```\n    node scripts/node-tests.js\n    ```\n\n### Test Coverage\n\n[![Coverage Status](https://coveralls.io/repos/github/getify/fasy/badge.svg?branch=master)](https://coveralls.io/github/getify/fasy?branch=master)\n\nIf you have [NYC (Istanbul)](https://github.com/istanbuljs/nyc) already installed on your system (requires v14.1+), you can use it to check the test coverage:\n\n```\nnpm run coverage\n```\n\nThen open up `coverage/lcov-report/index.html` in a browser to view the report.\n\n**Note:** The npm script `coverage:report` is only intended for use by project maintainers. It sends coverage reports to [Coveralls](https://coveralls.io/).\n\n## License\n\n[![License](https://img.shields.io/badge/license-MIT-a1356a)](LICENSE.txt)\n\nAll code and documentation are (c) 2021 Kyle Simpson and released under the [MIT License](http://getify.mit-license.org/). A copy of the MIT License [is also included](LICENSE.txt).\n","funding_links":["https://github.com/sponsors/getify","https://patreon.com/getify","https://www.paypal.com/paypalme2/getify","https://www.blockchain.com/btc/address/32R5dVrqirdcbiyvUw85y7YbPFZTd7YpnH"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgetify%2Ffasy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgetify%2Ffasy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgetify%2Ffasy/lists"}