├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── scripts ├── FileSystem.ts ├── build.ts ├── pre-publish.ts ├── release.ts └── run.ts ├── src ├── __tests__ │ ├── remote-data-io.spec.ts │ ├── remote-data-t.spec.ts │ └── remote-data.spec.ts ├── index.ts ├── remote-data-io.ts ├── remote-data-t.ts └── remote-data.ts ├── tsconfig.build.json ├── tsconfig.es6.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.{js,ts,jsx,tsx,html,css,scss,sass}] 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [*.md] 14 | max_line_length = off 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /dist 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /package-lock.json 3 | /.editorconfig 4 | /.gitignore 5 | /.prettierrc 6 | /tslint.json 7 | /tsconfig.json 8 | /node_modules 9 | /*.log 10 | /.idea 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "jsxBracketSameLine": true, 4 | "parser": "typescript", 5 | "printWidth": 120, 6 | "semi": true, 7 | "singleQuote": true, 8 | "tabWidth": 4, 9 | "trailingComma": "all", 10 | "useTabs": true 11 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: 5 | - npm run test && npm run build 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.1.1](https://github.com/devexperts/remote-data-ts/compare/v2.0.4...v2.1.1) (2021-09-09) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * invalid main and typings path in package.json ([#68](https://github.com/devexperts/remote-data-ts/issues/68)) ([4ca54c3](https://github.com/devexperts/remote-data-ts/commit/4ca54c3c20bb5f0a795f6aac5b0f1d38afc47b69)) 7 | 8 | # [2.1.0](https://github.com/devexperts/remote-data-ts/compare/v2.0.4...v2.1.0) (2021-08-17) 9 | 10 | 11 | ### Features 12 | 13 | * add Fold 3 ([#53](https://github.com/devexperts/remote-data-ts/issues/53)) ([5c26c11](https://github.com/devexperts/remote-data-ts/commit/5c26c110456e9431db1f34c8440b7ddc58013333)), closes [#30](https://github.com/devexperts/remote-data-ts/issues/30) 14 | * Support for import without lib or es6 ([#65](https://github.com/devexperts/remote-data-ts/issues/65)) ([02b8f08](https://github.com/devexperts/remote-data-ts/commit/02b8f0891b0dcc2fafd42354e8755d51e55f9368)) 15 | 16 | 17 | * Update constructors to receive all generics (#61) ([674a2ac](https://github.com/devexperts/remote-data-ts/commit/674a2ac3f5f09f73aab5d8192fca64acf041aec6)), closes [#61](https://github.com/devexperts/remote-data-ts/issues/61) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * Correct `RemoteDataT3` constrant for `M` (`URIS4` -> `URIS3`) ([f81f3b8](https://github.com/devexperts/remote-data-ts/commit/f81f3b8d819bd0642ba00beebbe19e938e97a3fc)) 23 | 24 | 25 | ### BREAKING CHANGES 26 | 27 | * generic parameters changed for `success` and `failure` constructors 28 | 29 | 30 | 31 | ## [2.0.4](https://github.com/devexperts/remote-data-ts/compare/v2.0.3...v2.0.4) (2020-08-11) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * rewrite imports from lib to es6 in es6 output ([c6899d8](https://github.com/devexperts/remote-data-ts/commit/c6899d8e70ee5f3c617b0bf7756dfb2a7f0dda6e)), closes [#46](https://github.com/devexperts/remote-data-ts/issues/46) [#49](https://github.com/devexperts/remote-data-ts/issues/49) 37 | 38 | 39 | 40 | ## [2.0.3](https://github.com/devexperts/remote-data-ts/compare/v2.0.2...v2.0.3) (2020-01-24) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * fix bimap, mapLeft, extend to forward progress correctly ([#45](https://github.com/devexperts/remote-data-ts/issues/45)) ([be78635](https://github.com/devexperts/remote-data-ts/commit/be78635c4cf77a9dd1531b745f17638194b0e15a)) 46 | 47 | 48 | 49 | ## [2.0.2](https://github.com/devexperts/remote-data-ts/compare/v2.0.0...v2.0.2) (2020-01-17) 50 | 51 | 52 | 53 | # [2.0.0](https://github.com/devexperts/remote-data-ts/compare/v0.6.0...v2.0.0) (2019-08-23) 54 | 55 | 56 | ### Code Refactoring 57 | 58 | * fp-ts@2 support added ([#34](https://github.com/devexperts/remote-data-ts/issues/34)) ([b7ad152](https://github.com/devexperts/remote-data-ts/commit/b7ad152d5058129f05c9e5a1d901310250b1dbbe)) 59 | 60 | 61 | ### feature 62 | 63 | * classless ([#35](https://github.com/devexperts/remote-data-ts/issues/35)) ([7351a88](https://github.com/devexperts/remote-data-ts/commit/7351a880e2cd416449d33e870675e967a2f23916)) 64 | * update RemoteDataT ([#36](https://github.com/devexperts/remote-data-ts/issues/36)) ([4249fc2](https://github.com/devexperts/remote-data-ts/commit/4249fc2722a28727132ced10d255d327007b93b0)) 65 | 66 | 67 | ### BREAKING CHANGES 68 | 69 | * transformer was completely rewritten 70 | * removed classes 71 | * simplified `io-ts` codec 72 | * fp-ts and io-ts-types dependencies updated to latest stable version 73 | 74 | 75 | 76 | # [0.6.0](https://github.com/devexperts/remote-data-ts/compare/v0.5.0...v0.6.0) (2019-05-26) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * incorrect json pending type ([#32](https://github.com/devexperts/remote-data-ts/issues/32)) ([eba21eb](https://github.com/devexperts/remote-data-ts/commit/eba21eb7643741d01ce8776ea56b94b18103dc82)), closes [#31](https://github.com/devexperts/remote-data-ts/issues/31) 82 | 83 | 84 | ### Features 85 | 86 | * Add RemoteDataT transformer ([#29](https://github.com/devexperts/remote-data-ts/issues/29)) ([9d2d0f2](https://github.com/devexperts/remote-data-ts/commit/9d2d0f2bec494a033f10f2659eb456c0f781dcdd)) 87 | 88 | 89 | 90 | # [0.5.0](https://github.com/devexperts/remote-data-ts/compare/v0.3.1...v0.5.0) (2019-01-31) 91 | 92 | 93 | ### Bug Fixes 94 | 95 | * move peer dependencies to dependencies ([#24](https://github.com/devexperts/remote-data-ts/issues/24)) ([0812d29](https://github.com/devexperts/remote-data-ts/commit/0812d29796723f51ef8c1ea6c222b6e2291ab34e)), closes [#23](https://github.com/devexperts/remote-data-ts/issues/23) 96 | 97 | 98 | ### Features 99 | 100 | * add io-ts type ([#19](https://github.com/devexperts/remote-data-ts/issues/19)) ([7d6785f](https://github.com/devexperts/remote-data-ts/commit/7d6785f4211ee263dacc73100677ca5c0b1994d2)) 101 | * major update - Fail fast ap, TS/fp-ts/io-ts upgrade, Traversable2v, Bifunctor ([#28](https://github.com/devexperts/remote-data-ts/issues/28)) ([3955c17](https://github.com/devexperts/remote-data-ts/commit/3955c175e427dacdb87ec7351ea451b0b7c454ad)), closes [#26](https://github.com/devexperts/remote-data-ts/issues/26) 102 | * provide progress parameter via fold pending parameter ([#20](https://github.com/devexperts/remote-data-ts/issues/20)) ([1cf41ce](https://github.com/devexperts/remote-data-ts/commit/1cf41ceda67507d979068471214d687ffe8b967c)) 103 | * relax getMonoid dependencies to Semigroup instances instead of Monoid ([#21](https://github.com/devexperts/remote-data-ts/issues/21)) ([d7b060e](https://github.com/devexperts/remote-data-ts/commit/d7b060e9298af11419991629f5da28d44759f972)) 104 | 105 | 106 | ### BREAKING CHANGES 107 | 108 | * new status priority, new dependencies, see #28 for more info 109 | * add io-ts and io-ts-types to peer-dependencies 110 | 111 | 112 | 113 | ## [0.3.1](https://github.com/devexperts/remote-data-ts/compare/v0.3.0...v0.3.1) (2018-10-16) 114 | 115 | 116 | ### Features 117 | 118 | * Add toEither method ([#16](https://github.com/devexperts/remote-data-ts/issues/16)) ([a314bb5](https://github.com/devexperts/remote-data-ts/commit/a314bb53e753307879d069ad1591e819260a871b)) 119 | 120 | 121 | 122 | # [0.3.0](https://github.com/devexperts/remote-data-ts/compare/0.2.0...v0.3.0) (2018-09-25) 123 | 124 | 125 | ### Features 126 | 127 | * add "recover" method ([27d5591](https://github.com/devexperts/remote-data-ts/commit/27d559131dee5aa316b8c1e91d56db9176b1db05)), closes [#12](https://github.com/devexperts/remote-data-ts/issues/12) 128 | * Add progress to RemotePending ([4c89823](https://github.com/devexperts/remote-data-ts/commit/4c89823a66852ea0bb4924e95603d5c2bad388f8)), closes [#9](https://github.com/devexperts/remote-data-ts/issues/9) 129 | * update to TS@2.8.1, fp-ts@1.2.0 + implement Monoidal ([d9a4a09](https://github.com/devexperts/remote-data-ts/commit/d9a4a09296940dbadd54c3b8c32aeaba31a7686d)) 130 | 131 | 132 | 133 | # 0.2.0 (2018-03-20) 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RemoteData type [![Build Status](https://travis-ci.org/devexperts/remote-data-ts.svg?branch=master)](https://travis-ci.org/devexperts/remote-data-ts) 2 | ### Description 3 | RemoteData is an ADT (algebraic data type) described in [this article](https://medium.com/@gcanti/slaying-a-ui-antipattern-with-flow-5eed0cfb627b). Heavily based on [fp-ts](https://github.com/gcanti/fp-ts) lib. 4 | 5 | ### Installation 6 | `npm i --save @devexperts/remote-data-ts` 7 | 8 | ### How to lift (wrap) your data in RemoteData: 9 | As you remember RemoteData is an union of few types: `RemoteInitial`, `RemotePending`, `RemoteFailure` and `RemoteSuccess`. 10 | 11 | While your data in **initial** or **pending** state just use `initial` or `pending` constant, because you don't have any **real** values in this case. 12 | 13 | ```ts 14 | import { initial, pending } from '@devexperts/remote-data-ts'; 15 | 16 | const customers = initial; 17 | // or 18 | const customers = pending; 19 | ``` 20 | 21 | When you receive data from server, use `failure` or `success` function, it depends on what you received: 22 | 23 | ```ts 24 | import { failure, success } from '@devexperts/remote-data-ts'; 25 | import { apiClient } from 'apiClient'; 26 | import { TCustomer } from './MyModel'; 27 | 28 | const getCustomers = (): RemoteData => { 29 | const rawData: TCustomer[] = apiClient.get('/customers'); 30 | 31 | try { 32 | const length = rawData.length; 33 | 34 | return success(rawData); 35 | } 36 | catch(err) { 37 | return failure(new Error('parse error')); 38 | } 39 | } 40 | ``` 41 | 42 | ### How to fold (unwrap) your data from RemoteData: 43 | Finally you pass data to the component and want to render values, so now it's time to get our values back from RemoteData wrapper: 44 | 45 | ```ts 46 | import { NoData, Pending, Failure } from './MyPlaceholders'; 47 | import { TCustomer } from './MyModel'; 48 | 49 | type TCustomersList = { 50 | entities: RemoteData; 51 | }; 52 | 53 | const CustomersList: SFC = ({ entities }) => entities.foldL( 54 | () => , 55 | () => , 56 | err => , 57 | data =>
    {data.map(item =>
  • {item.name}
  • )}
58 | ); 59 | ``` 60 | 61 | ### Docs & Examples 62 | Coming soon (check the [source](src/remote-data.ts)) 63 | 64 | 65 | ### Contributions 66 | - use https://www.conventionalcommits.org/en/v1.0.0-beta.2/ 67 | 68 | ### Publish 69 | Don't forget to run `npm run changelog` and to commit the changes 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devexperts/remote-data-ts", 3 | "version": "2.1.1", 4 | "main": "lib/index.js", 5 | "module": "es6/index.js", 6 | "typings": "lib/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "tsc -p ./tsconfig.build.json && tsc -p ./tsconfig.es6.json && npm run import-path-rewrite && ts-node scripts/build", 10 | "test": "npm run tslint && npm run prettier && npm run jest", 11 | "tslint": "tslint -c tslint.json --project tsconfig.json './src/**/*.ts'", 12 | "jest": "jest", 13 | "prettier": "prettier --list-different \"./{src,scripts}/**/*.ts\"", 14 | "prettier:fix": "prettier --write \"./{src,scripts}/**/*.ts\"", 15 | "prepublishOnly": "ts-node scripts/pre-publish", 16 | "prerelease": "npm run build", 17 | "import-path-rewrite": "import-path-rewrite", 18 | "release": "ts-node scripts/release", 19 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 20 | "version": "npm run changelog && git add CHANGELOG.md" 21 | }, 22 | "author": "devexperts", 23 | "license": "MPL-2.0", 24 | "devDependencies": { 25 | "@devexperts/lint": "^0.29.1", 26 | "@types/glob": "^7.1.3", 27 | "@types/jest": "^22.2.3", 28 | "conventional-changelog-cli": "^2.0.21", 29 | "import-path-rewrite": "github:gcanti/import-path-rewrite", 30 | "jest": "^24.8.0", 31 | "jest-cli": "^24.8.0", 32 | "fp-ts": "^2.5.0", 33 | "glob": "^7.1.7", 34 | "io-ts": "^2.0.0", 35 | "io-ts-types": "^0.5.7", 36 | "prettier": "^1.17.1", 37 | "ts-jest": "^23.10.5", 38 | "tslint": "^5.16.0", 39 | "tslint-config-prettier": "^1.18.0", 40 | "tslint-plugin-prettier": "^1.3.0", 41 | "ts-node": "8.8.2", 42 | "typescript": "^3.5.2" 43 | }, 44 | "peerDependencies": { 45 | "fp-ts": "^2.0.0", 46 | "io-ts": "^2.0.0", 47 | "io-ts-types": "^0.5.7" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "git+https://github.com/devexperts/remote-data-ts.git" 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/devexperts/remote-data-ts/issues" 55 | }, 56 | "homepage": "https://github.com/devexperts/remote-data-ts#readme", 57 | "description": "RemoteData type", 58 | "tags": [ 59 | "typescript", 60 | "algebraic-data-types", 61 | "functional-programming" 62 | ], 63 | "keywords": [ 64 | "typescript", 65 | "algebraic-data-types", 66 | "functional-programming" 67 | ], 68 | "publishConfig": { 69 | "access": "public" 70 | }, 71 | "jest": { 72 | "transform": { 73 | "^.+\\.tsx?$": "ts-jest" 74 | }, 75 | "testRegex": "(/__tests__/.*|(\\.|/)spec)\\.ts$", 76 | "testPathIgnorePatterns": [ 77 | "/dist" 78 | ], 79 | "moduleFileExtensions": [ 80 | "ts", 81 | "js" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /scripts/FileSystem.ts: -------------------------------------------------------------------------------- 1 | import * as TE from 'fp-ts/lib/TaskEither'; 2 | import { flow } from 'fp-ts/lib/function'; 3 | import * as fs from 'fs'; 4 | import G from 'glob'; 5 | 6 | export interface FileSystem { 7 | readonly readFile: (path: string) => TE.TaskEither; 8 | readonly writeFile: (path: string, content: string) => TE.TaskEither; 9 | readonly copyFile: (from: string, to: string) => TE.TaskEither; 10 | readonly glob: (pattern: string) => TE.TaskEither>; 11 | readonly mkdir: (path: string) => TE.TaskEither; 12 | } 13 | 14 | const readFile = TE.taskify(fs.readFile); 15 | const writeFile = TE.taskify(fs.writeFile); 16 | const copyFile = TE.taskify(fs.copyFile); 17 | const glob = TE.taskify>(G); 18 | const mkdirTE = TE.taskify(fs.mkdir); 19 | 20 | export const fileSystem: FileSystem = { 21 | readFile: path => readFile(path, 'utf8'), 22 | writeFile, 23 | copyFile, 24 | glob, 25 | mkdir: flow( 26 | mkdirTE, 27 | TE.map(() => undefined), 28 | ), 29 | }; 30 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as E from 'fp-ts/lib/Either'; 3 | import { pipe } from 'fp-ts/lib/pipeable'; 4 | import * as RTE from 'fp-ts/lib/ReaderTaskEither'; 5 | import * as A from 'fp-ts/lib/ReadonlyArray'; 6 | import * as TE from 'fp-ts/lib/TaskEither'; 7 | import { FileSystem, fileSystem } from './FileSystem'; 8 | import { run } from './run'; 9 | 10 | interface Build extends RTE.ReaderTaskEither {} 11 | 12 | const OUTPUT_FOLDER = 'dist'; 13 | const PKG = 'package.json'; 14 | 15 | export const copyPackageJson: Build = C => 16 | pipe( 17 | C.readFile(PKG), 18 | TE.chain(s => TE.fromEither(E.parseJSON(s, E.toError))), 19 | TE.map(v => { 20 | const clone = Object.assign({}, v as any); 21 | 22 | delete clone.scripts; 23 | delete clone.files; 24 | delete clone.devDependencies; 25 | 26 | return clone; 27 | }), 28 | TE.chain(json => C.writeFile(path.join(OUTPUT_FOLDER, PKG), JSON.stringify(json, null, 2))), 29 | ); 30 | 31 | export const FILES: ReadonlyArray = ['CHANGELOG.md', 'LICENSE', 'README.md']; 32 | 33 | export const copyFiles: Build> = C => 34 | A.readonlyArray.traverse(TE.taskEither)(FILES, from => C.copyFile(from, path.resolve(OUTPUT_FOLDER, from))); 35 | 36 | const traverse = A.readonlyArray.traverse(TE.taskEither); 37 | 38 | export const makeModules: Build = C => 39 | pipe( 40 | C.glob(`${OUTPUT_FOLDER}/lib/*.js`), 41 | TE.map(getModules), 42 | TE.chain(modules => traverse(modules, makeSingleModule(C))), 43 | TE.map(() => undefined), 44 | ); 45 | 46 | function getModules(paths: ReadonlyArray): ReadonlyArray { 47 | return paths.map(filePath => path.basename(filePath, '.js')).filter(x => x !== 'index'); 48 | } 49 | 50 | function makeSingleModule(C: FileSystem): (module: string) => TE.TaskEither { 51 | return m => 52 | pipe( 53 | C.mkdir(path.join(OUTPUT_FOLDER, m)), 54 | TE.chain(() => makePkgJson(m)), 55 | TE.chain(data => C.writeFile(path.join(OUTPUT_FOLDER, m, 'package.json'), data)), 56 | ); 57 | } 58 | 59 | function makePkgJson(module: string): TE.TaskEither { 60 | return pipe( 61 | JSON.stringify( 62 | { 63 | main: `../lib/${module}.js`, 64 | module: `../es6/${module}.js`, 65 | typings: `../lib/${module}.d.ts`, 66 | sideEffects: false, 67 | }, 68 | null, 69 | 2, 70 | ), 71 | TE.right, 72 | ); 73 | } 74 | 75 | const main: Build = pipe( 76 | copyPackageJson, 77 | RTE.chain(() => copyFiles), 78 | RTE.chain(() => makeModules), 79 | ); 80 | 81 | run( 82 | main({ 83 | ...fileSystem, 84 | }), 85 | ); 86 | -------------------------------------------------------------------------------- /scripts/pre-publish.ts: -------------------------------------------------------------------------------- 1 | import { left } from 'fp-ts/lib/TaskEither'; 2 | import { run } from './run'; 3 | 4 | const main = left(new Error('"npm publish" can not be run from root, run "npm run release" instead')); 5 | 6 | run(main); 7 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import { run } from './run'; 2 | import * as child_process from 'child_process'; 3 | import { left, right } from 'fp-ts/lib/Either'; 4 | import * as TE from 'fp-ts/lib/TaskEither'; 5 | 6 | const DIST = 'dist'; 7 | 8 | const exec = (cmd: string, args?: child_process.ExecOptions): TE.TaskEither => () => 9 | new Promise(resolve => { 10 | child_process.exec(cmd, args, err => { 11 | if (err !== null) { 12 | return resolve(left(err)); 13 | } 14 | 15 | return resolve(right(undefined)); 16 | }); 17 | }); 18 | 19 | export const main = exec('npm publish', { 20 | cwd: DIST, 21 | }); 22 | 23 | run(main); 24 | -------------------------------------------------------------------------------- /scripts/run.ts: -------------------------------------------------------------------------------- 1 | import { fold } from 'fp-ts/lib/Either'; 2 | import { TaskEither } from 'fp-ts/lib/TaskEither'; 3 | 4 | export function run(eff: TaskEither): void { 5 | eff() 6 | .then( 7 | fold( 8 | e => { 9 | throw e; 10 | }, 11 | _ => { 12 | process.exitCode = 0; 13 | }, 14 | ), 15 | ) 16 | .catch(e => { 17 | console.error(e); // tslint:disable-line no-console 18 | 19 | process.exitCode = 1; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/__tests__/remote-data-io.spec.ts: -------------------------------------------------------------------------------- 1 | import { createRemoteDataFromJSON } from '../remote-data-io'; 2 | import { initial, pending, failure, success, progress } from '../remote-data'; 3 | import { right } from 'fp-ts/lib/Either'; 4 | import { none, some } from 'fp-ts/lib/Option'; 5 | import { number, string } from 'io-ts'; 6 | 7 | describe('RemoteDataFromJSONType', () => { 8 | it('createRemoteDataFromJSON', () => { 9 | const codec = createRemoteDataFromJSON(string, number); 10 | expect(codec.decode({ _tag: 'RemoteFailure', error: 'error' })).toEqual(right(failure('error'))); 11 | expect(codec.decode({ _tag: 'RemoteInitial' })).toEqual(right(initial)); 12 | expect(codec.decode({ _tag: 'RemotePending', progress: null })).toEqual(right(pending)); 13 | expect(codec.decode({ _tag: 'RemotePending', progress: { loaded: 2, total: null } })).toEqual( 14 | right(progress({ loaded: 2, total: none })), 15 | ); 16 | expect(codec.decode({ _tag: 'RemotePending', progress: { loaded: 2, total: 5 } })).toEqual( 17 | right(progress({ loaded: 2, total: some(5) })), 18 | ); 19 | expect(codec.decode({ _tag: 'RemoteSuccess', value: 42 })).toEqual(right(success(42))); 20 | expect(codec.encode(failure('error'))).toEqual({ _tag: 'RemoteFailure', error: 'error' }); 21 | expect(codec.encode(initial)).toEqual({ _tag: 'RemoteInitial' }); 22 | expect(codec.encode(pending)).toEqual({ _tag: 'RemotePending', progress: null }); 23 | expect(codec.encode(progress({ loaded: 2, total: none }))).toEqual({ 24 | _tag: 'RemotePending', 25 | progress: { loaded: 2, total: null }, 26 | }); 27 | expect(codec.encode(progress({ loaded: 2, total: some(5) }))).toEqual({ 28 | _tag: 'RemotePending', 29 | progress: { loaded: 2, total: 5 }, 30 | }); 31 | expect(codec.encode(success(42))).toEqual({ _tag: 'RemoteSuccess', value: 42 }); 32 | expect(codec.is(failure('error'))).toBe(true); 33 | expect(codec.is(initial)).toBe(true); 34 | expect(codec.is(pending)).toBe(true); 35 | expect(codec.is(success(42))).toBe(true); 36 | expect(codec.is(failure(1))).toBe(false); 37 | expect(codec.is(success('invalid'))).toBe(false); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/__tests__/remote-data-t.spec.ts: -------------------------------------------------------------------------------- 1 | import { pipe, pipeable } from 'fp-ts/lib/pipeable'; 2 | import { getRemoteDataM, RemoteDataM1, RemoteDataT1 } from '../remote-data-t'; 3 | import { flow, identity } from 'fp-ts/lib/function'; 4 | import { none, option, some } from 'fp-ts/lib/Option'; 5 | import { array } from 'fp-ts/lib/Array'; 6 | import { Monad2 } from 'fp-ts/lib/Monad'; 7 | 8 | declare module 'fp-ts/lib/HKT' { 9 | interface URItoKind2 { 10 | RemoteDataT1Option: RemoteDataT1<'Option', E, A>; 11 | } 12 | } 13 | const M: RemoteDataM1<'Option'> & Monad2<'RemoteDataT1Option'> = { 14 | URI: 'RemoteDataT1Option', 15 | ...getRemoteDataM(option), 16 | }; 17 | const P = pipeable(M); 18 | const double = (x: number) => x * 2; 19 | const successM = M.of(1); 20 | const failureM = M.failure('foo'); 21 | const progressM = M.progress({ loaded: 1, total: none }); 22 | 23 | describe('RemoteDataT', () => { 24 | describe('typeclasses', () => { 25 | describe('Functor', () => { 26 | describe('should map over value', () => { 27 | it('initial', async () => { 28 | expect(M.map(M.initial, double)).toEqual(M.initial); 29 | }); 30 | it('pending', async () => { 31 | expect(M.map(M.pending, double)).toEqual(M.pending); 32 | }); 33 | it('failure', async () => { 34 | const failed = M.failure('foo'); 35 | expect(M.map(failed, double)).toEqual(failed); 36 | }); 37 | it('success', async () => { 38 | const value = 123; 39 | const succeeded = M.of(value); 40 | const result = M.map(succeeded, double); 41 | expect(result).toEqual(M.of(value * 2)); 42 | }); 43 | }); 44 | describe('laws', () => { 45 | describe('identity', () => { 46 | it('initial', () => { 47 | expect(M.map(M.initial, identity)).toEqual(M.initial); 48 | }); 49 | it('pending', () => { 50 | expect(M.map(M.pending, identity)).toEqual(M.pending); 51 | }); 52 | it('failure', () => { 53 | const failed = M.failure('foo'); 54 | expect(M.map(failed, identity)).toEqual(failed); 55 | }); 56 | it('success', () => { 57 | const succeeded = M.of('foo'); 58 | const result = M.map(succeeded, identity); 59 | expect(result).toEqual(succeeded); 60 | }); 61 | }); 62 | describe('composition', () => { 63 | const double = (a: number): number => a * 2; 64 | const quad = flow( 65 | double, 66 | double, 67 | ); 68 | it('initial', () => { 69 | expect(M.map(M.initial, quad)).toEqual( 70 | pipe( 71 | M.initial, 72 | P.map(double), 73 | P.map(double), 74 | ), 75 | ); 76 | }); 77 | it('pending', () => { 78 | expect(M.map(M.pending, quad)).toEqual( 79 | pipe( 80 | M.pending, 81 | P.map(double), 82 | P.map(double), 83 | ), 84 | ); 85 | }); 86 | it('failure', () => { 87 | const failed = M.failure('foo'); 88 | expect(M.map(failed, quad)).toEqual( 89 | pipe( 90 | failed, 91 | P.map(double), 92 | P.map(double), 93 | ), 94 | ); 95 | }); 96 | it('success', () => { 97 | const value = 1; 98 | const succeeded = M.of(value); 99 | expect(M.map(succeeded, quad)).toEqual(M.of(quad(value))); 100 | }); 101 | }); 102 | }); 103 | }); 104 | describe('Alt', () => { 105 | describe('should alt', () => { 106 | it('initial', () => { 107 | expect(M.alt(M.initial, () => M.initial)).toEqual(M.initial); 108 | expect(M.alt(M.initial, () => M.pending)).toEqual(M.pending); 109 | expect(M.alt(M.initial, () => failureM)).toEqual(failureM); 110 | expect(M.alt(M.initial, () => successM)).toEqual(successM); 111 | }); 112 | it('pending', () => { 113 | expect(M.alt(M.pending, () => M.initial)).toEqual(M.initial); 114 | expect(M.alt(M.pending, () => M.pending)).toEqual(M.pending); 115 | expect(M.alt(M.pending, () => failureM)).toEqual(failureM); 116 | expect(M.alt(M.pending, () => successM)).toEqual(successM); 117 | }); 118 | it('failure', () => { 119 | expect(M.alt(failureM, () => M.pending)).toEqual(M.pending); 120 | expect(M.alt(failureM, () => M.initial)).toEqual(M.initial); 121 | expect(M.alt(failureM, () => failureM)).toEqual(failureM); 122 | expect(M.alt(failureM, () => successM)).toEqual(successM); 123 | }); 124 | it('failure', () => { 125 | expect(M.alt(successM, () => M.pending)).toEqual(successM); 126 | expect(M.alt(successM, () => M.initial)).toEqual(successM); 127 | expect(M.alt(successM, () => failureM)).toEqual(successM); 128 | expect(M.alt(successM, () => successM)).toEqual(successM); 129 | }); 130 | }); 131 | }); 132 | describe('Apply', () => { 133 | describe('should ap', () => { 134 | const f = M.of(double); 135 | const failedF = M.failure('foo'); 136 | it('initial', () => { 137 | expect(M.ap(M.initial, M.initial)).toEqual(M.initial); 138 | expect(M.ap(M.pending, M.initial)).toEqual(M.initial); 139 | expect(M.ap(progressM, M.initial)).toEqual(M.initial); 140 | expect(M.ap(failedF, M.initial)).toEqual(failedF); 141 | expect(M.ap(f, M.initial)).toEqual(M.initial); 142 | }); 143 | it('pending', () => { 144 | expect(M.ap(M.initial, M.pending)).toEqual(M.initial); 145 | expect(M.ap(M.pending, M.pending)).toEqual(M.pending); 146 | expect(M.ap(progressM, M.pending)).toEqual(progressM); 147 | expect(M.ap(failedF, M.pending)).toEqual(failedF); 148 | expect(M.ap(f, M.pending)).toEqual(M.pending); 149 | }); 150 | it('failure', () => { 151 | expect(M.ap(M.initial, failureM)).toEqual(failureM); 152 | expect(M.ap(M.pending, failureM)).toEqual(failureM); 153 | expect(M.ap(progressM, failureM)).toEqual(failureM); 154 | expect(M.ap(failedF, failureM)).toEqual(failedF); 155 | expect(M.ap(f, failureM)).toEqual(failureM); 156 | }); 157 | it('success', () => { 158 | expect(M.ap(M.initial, successM)).toEqual(M.initial); 159 | expect(M.ap(M.pending, successM)).toEqual(M.pending); 160 | expect(M.ap(progressM, successM)).toEqual(progressM); 161 | expect(M.ap(failedF, successM)).toEqual(failedF); 162 | expect(M.ap(f, successM)).toEqual(M.of(double(1))); 163 | }); 164 | }); 165 | }); 166 | describe('Applicative', () => { 167 | describe('sequence', () => { 168 | const s = array.sequence(M); 169 | it('initial', () => { 170 | expect(s([M.initial, successM])).toEqual(M.initial); 171 | }); 172 | it('pending', () => { 173 | expect(s([M.pending, successM])).toEqual(M.pending); 174 | }); 175 | it('failure', () => { 176 | expect(s([failureM, successM])).toEqual(failureM); 177 | }); 178 | it('success', () => { 179 | expect(s([M.of(123), M.of(456)])).toEqual(M.of([123, 456])); 180 | }); 181 | }); 182 | }); 183 | describe('Chain', () => { 184 | describe('chain', () => { 185 | it('initial', () => { 186 | expect(M.chain(M.initial, () => M.initial)).toEqual(M.initial); 187 | expect(M.chain(M.initial, () => M.pending)).toEqual(M.initial); 188 | expect(M.chain(M.initial, () => failureM)).toEqual(M.initial); 189 | expect(M.chain(M.initial, () => successM)).toEqual(M.initial); 190 | }); 191 | it('pending', () => { 192 | expect(M.chain(M.pending, () => M.initial)).toEqual(M.pending); 193 | expect(M.chain(M.pending, () => M.pending)).toEqual(M.pending); 194 | expect(M.chain(M.pending, () => failureM)).toEqual(M.pending); 195 | expect(M.chain(M.pending, () => successM)).toEqual(M.pending); 196 | }); 197 | it('failure', () => { 198 | expect(M.chain(failureM, () => M.initial)).toEqual(failureM); 199 | expect(M.chain(failureM, () => M.pending)).toEqual(failureM); 200 | expect(M.chain(failureM, () => failureM)).toEqual(failureM); 201 | expect(M.chain(failureM, () => successM)).toEqual(failureM); 202 | }); 203 | it('success', () => { 204 | expect(M.chain(successM, () => M.initial)).toEqual(M.initial); 205 | expect(M.chain(successM, () => M.pending)).toEqual(M.pending); 206 | expect(M.chain(successM, () => failureM)).toEqual(failureM); 207 | expect(M.chain(successM, () => successM)).toEqual(successM); 208 | }); 209 | }); 210 | }); 211 | describe('Bifunctor', () => { 212 | describe('bimap', () => { 213 | const f = (l: string): string => `Error: ${l}`; 214 | const g = (a: number): number => a + 1; 215 | it('initial', () => { 216 | expect(M.bimap(M.initial, f, g)).toEqual(M.initial); 217 | expect(M.bimap(M.initial, identity, identity)).toEqual(M.initial); 218 | }); 219 | it('pending', () => { 220 | expect(M.bimap(M.pending, f, g)).toEqual(M.pending); 221 | expect(M.bimap(M.pending, identity, identity)).toEqual(M.pending); 222 | }); 223 | it('failure', () => { 224 | expect(M.bimap(failureM, f, g)).toEqual(M.mapLeft(failureM, f)); 225 | expect(M.bimap(failureM, f, g)).toEqual(M.failure('Error: foo')); 226 | expect(M.bimap(failureM, identity, identity)).toEqual(failureM); 227 | }); 228 | it('success', () => { 229 | expect(M.bimap(successM, f, g)).toEqual(M.map(successM, g)); 230 | expect(M.bimap(successM, f, g)).toEqual(M.of(2)); 231 | expect(M.bimap(successM, identity, identity)).toEqual(successM); 232 | }); 233 | }); 234 | describe('mapLeft', () => { 235 | const f2 = () => 1; 236 | it('initial', () => { 237 | expect(M.mapLeft(M.initial, f2)).toEqual(M.initial); 238 | }); 239 | it('pending', () => { 240 | expect(M.mapLeft(M.pending, f2)).toEqual(M.pending); 241 | }); 242 | it('failure', () => { 243 | expect(M.mapLeft(failureM, f2)).toEqual(M.failure(1)); 244 | }); 245 | it('success', () => { 246 | expect(M.mapLeft(successM, f2)).toEqual(successM); 247 | }); 248 | }); 249 | }); 250 | }); 251 | 252 | describe('top-level', () => { 253 | describe('fromOption', () => { 254 | const error = new Error('foo'); 255 | it('none', () => { 256 | expect(M.fromOption(none, () => error)).toEqual(M.failure(error)); 257 | }); 258 | it('some', () => { 259 | expect(M.fromOption(some(123), () => error)).toEqual(M.of(123)); 260 | }); 261 | }); 262 | }); 263 | }); 264 | -------------------------------------------------------------------------------- /src/__tests__/remote-data.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | pending, 3 | failure, 4 | success, 5 | RemoteData, 6 | initial, 7 | combine, 8 | remoteData, 9 | getEq, 10 | getOrd, 11 | getSemigroup, 12 | getMonoid, 13 | fromOption, 14 | fromEither, 15 | fromPredicate, 16 | progress, 17 | fromProgressEvent, 18 | mapLeft, 19 | isInitial, 20 | isPending, 21 | isFailure, 22 | isSuccess, 23 | toOption, 24 | toEither, 25 | toNullable, 26 | getShow, 27 | elem, 28 | exists, 29 | recoverMap, 30 | recover, 31 | } from '../remote-data'; 32 | import { identity, flow, FunctionN } from 'fp-ts/lib/function'; 33 | import { none, option, some } from 'fp-ts/lib/Option'; 34 | import { array } from 'fp-ts/lib/Array'; 35 | import { eqNumber, eqString } from 'fp-ts/lib/Eq'; 36 | import { ordNumber, ordString } from 'fp-ts/lib/Ord'; 37 | import { semigroupString, semigroupSum } from 'fp-ts/lib/Semigroup'; 38 | import { monoidString, monoidSum } from 'fp-ts/lib/Monoid'; 39 | import { left, right } from 'fp-ts/lib/Either'; 40 | import * as RD from '../remote-data'; 41 | import { pipe } from 'fp-ts/lib/pipeable'; 42 | import { showNumber, showString } from 'fp-ts/lib/Show'; 43 | 44 | describe('RemoteData', () => { 45 | const double = (x: number) => x * 2; 46 | const initialRD: RemoteData = initial; 47 | const pendingRD: RemoteData = pending; 48 | const successRD: RemoteData = success(1); 49 | const failureRD: RemoteData = failure('foo'); 50 | const progressRD: RemoteData> = progress({ loaded: 1, total: none }); 51 | describe('typeclasses', () => { 52 | describe('Functor', () => { 53 | describe('should map over value', () => { 54 | it('initial', () => { 55 | expect(remoteData.map(initial, double)).toBe(initial); 56 | }); 57 | it('pending', () => { 58 | expect(remoteData.map(pending, double)).toBe(pending); 59 | }); 60 | it('failure', () => { 61 | const failed = failure('foo'); 62 | expect(remoteData.map(failed, double)).toBe(failed); 63 | }); 64 | it('success', () => { 65 | const value = 123; 66 | const succeeded = success(value); 67 | const result = remoteData.map(succeeded, double); 68 | expect(result).toEqual(success(value * 2)); 69 | }); 70 | }); 71 | describe('laws', () => { 72 | describe('identity', () => { 73 | it('initial', () => { 74 | expect(remoteData.map(initial, identity)).toBe(initial); 75 | }); 76 | it('pending', () => { 77 | expect(remoteData.map(pending, identity)).toBe(pending); 78 | }); 79 | it('failure', () => { 80 | const failed = failure('foo'); 81 | expect(remoteData.map(failed, identity)).toBe(failed); 82 | }); 83 | it('success', () => { 84 | const succeeded = success('foo'); 85 | const result = remoteData.map(succeeded, identity); 86 | expect(result).toEqual(succeeded); 87 | expect(result).not.toBe(succeeded); 88 | }); 89 | }); 90 | describe('composition', () => { 91 | const double = (a: number): number => a * 2; 92 | const quad = flow( 93 | double, 94 | double, 95 | ); 96 | it('initial', () => { 97 | expect(remoteData.map(initial, quad)).toBe( 98 | pipe( 99 | initial, 100 | RD.map(double), 101 | RD.map(double), 102 | ), 103 | ); 104 | }); 105 | it('pending', () => { 106 | expect(remoteData.map(pending, quad)).toBe( 107 | pipe( 108 | pending, 109 | RD.map(double), 110 | RD.map(double), 111 | ), 112 | ); 113 | }); 114 | it('failure', () => { 115 | const failed: RemoteData = failure('foo'); 116 | expect(remoteData.map(failed, quad)).toBe( 117 | pipe( 118 | failed, 119 | RD.map(double), 120 | RD.map(double), 121 | ), 122 | ); 123 | }); 124 | it('success', () => { 125 | const value = 1; 126 | const succeeded = success(value); 127 | expect(remoteData.map(succeeded, quad)).toEqual(success(quad(value))); 128 | }); 129 | }); 130 | }); 131 | }); 132 | describe('Alt', () => { 133 | describe('should alt', () => { 134 | it('initial', () => { 135 | expect(remoteData.alt(initialRD, () => initialRD)).toBe(initialRD); 136 | expect(remoteData.alt(initialRD, () => pendingRD)).toBe(pendingRD); 137 | expect(remoteData.alt(initialRD, () => failureRD)).toBe(failureRD); 138 | expect(remoteData.alt(initialRD, () => successRD)).toBe(successRD); 139 | }); 140 | it('pending', () => { 141 | expect(remoteData.alt(pendingRD, () => initialRD)).toBe(initialRD); 142 | expect(remoteData.alt(pendingRD, () => pendingRD)).toBe(pendingRD); 143 | expect(remoteData.alt(pendingRD, () => failureRD)).toBe(failureRD); 144 | expect(remoteData.alt(pendingRD, () => successRD)).toBe(successRD); 145 | }); 146 | it('failure', () => { 147 | expect(remoteData.alt(failureRD, () => pendingRD)).toBe(pendingRD); 148 | expect(remoteData.alt(failureRD, () => initialRD)).toBe(initialRD); 149 | expect(remoteData.alt(failureRD, () => failureRD)).toBe(failureRD); 150 | expect(remoteData.alt(failureRD, () => successRD)).toBe(successRD); 151 | }); 152 | it('failure', () => { 153 | expect(remoteData.alt(successRD, () => pendingRD)).toBe(successRD); 154 | expect(remoteData.alt(successRD, () => initialRD)).toBe(successRD); 155 | expect(remoteData.alt(successRD, () => failureRD)).toBe(successRD); 156 | expect(remoteData.alt(successRD, () => successRD)).toBe(successRD); 157 | }); 158 | }); 159 | }); 160 | describe('Apply', () => { 161 | describe('should ap', () => { 162 | const f: RemoteData number> = success(double); 163 | const failedF: RemoteData number> = failure('foo'); 164 | it('initial', () => { 165 | expect(remoteData.ap(initial, initialRD)).toBe(initialRD); 166 | expect(remoteData.ap(pending, initialRD)).toBe(initialRD); 167 | expect(remoteData.ap(progressRD, initialRD)).toBe(initialRD); 168 | expect(remoteData.ap(failedF, initialRD)).toBe(failedF); 169 | expect(remoteData.ap(f, initialRD)).toBe(initialRD); 170 | }); 171 | it('pending', () => { 172 | expect(remoteData.ap(initial, pendingRD)).toBe(initial); 173 | expect(remoteData.ap(pending, pendingRD)).toBe(pendingRD); 174 | expect(remoteData.ap(progressRD, pendingRD)).toBe(progressRD); 175 | expect(remoteData.ap(failedF, pendingRD)).toBe(failedF); 176 | expect(remoteData.ap(f, pendingRD)).toBe(pendingRD); 177 | }); 178 | it('failure', () => { 179 | expect(remoteData.ap(initial, failureRD)).toBe(failureRD); 180 | expect(remoteData.ap(pending, failureRD)).toBe(failureRD); 181 | expect(remoteData.ap(progressRD, failureRD)).toBe(failureRD); 182 | expect(remoteData.ap(failedF, failureRD)).toBe(failedF); 183 | expect(remoteData.ap(f, failureRD)).toBe(failureRD); 184 | }); 185 | it('success', () => { 186 | expect(remoteData.ap(initial, successRD)).toBe(initial); 187 | expect(remoteData.ap(pending, successRD)).toBe(pending); 188 | expect(remoteData.ap(progressRD, successRD)).toBe(progressRD); 189 | expect(remoteData.ap(failedF, successRD)).toBe(failedF); 190 | expect(remoteData.ap(f, successRD)).toEqual(success(double(1))); 191 | }); 192 | }); 193 | }); 194 | describe('Applicative', () => { 195 | describe('sequence', () => { 196 | const s = array.sequence(remoteData); 197 | it('initial', () => { 198 | expect(s([initialRD, successRD])).toBe(initialRD); 199 | }); 200 | it('pending', () => { 201 | expect(s([pendingRD, successRD])).toBe(pendingRD); 202 | }); 203 | it('failure', () => { 204 | expect(s([failureRD, successRD])).toBe(failureRD); 205 | }); 206 | it('success', () => { 207 | expect(s([success(123), success(456)])).toEqual(success([123, 456])); 208 | }); 209 | }); 210 | }); 211 | describe('Chain', () => { 212 | describe('chain', () => { 213 | it('initial', () => { 214 | expect(remoteData.chain(initialRD, () => initialRD)).toBe(initialRD); 215 | expect(remoteData.chain(initialRD, () => pendingRD)).toBe(initialRD); 216 | expect(remoteData.chain(initialRD, () => failureRD)).toBe(initialRD); 217 | expect(remoteData.chain(initialRD, () => successRD)).toBe(initialRD); 218 | }); 219 | it('pending', () => { 220 | expect(remoteData.chain(pendingRD, () => initialRD)).toBe(pendingRD); 221 | expect(remoteData.chain(pendingRD, () => pendingRD)).toBe(pendingRD); 222 | expect(remoteData.chain(pendingRD, () => failureRD)).toBe(pendingRD); 223 | expect(remoteData.chain(pendingRD, () => successRD)).toBe(pendingRD); 224 | }); 225 | it('failure', () => { 226 | expect(remoteData.chain(failureRD, () => initialRD)).toBe(failureRD); 227 | expect(remoteData.chain(failureRD, () => pendingRD)).toBe(failureRD); 228 | expect(remoteData.chain(failureRD, () => failureRD)).toBe(failureRD); 229 | expect(remoteData.chain(failureRD, () => successRD)).toBe(failureRD); 230 | }); 231 | it('success', () => { 232 | expect(remoteData.chain(successRD, () => initialRD)).toBe(initialRD); 233 | expect(remoteData.chain(successRD, () => pendingRD)).toBe(pendingRD); 234 | expect(remoteData.chain(successRD, () => failureRD)).toBe(failureRD); 235 | expect(remoteData.chain(successRD, () => successRD)).toBe(successRD); 236 | }); 237 | }); 238 | }); 239 | describe('Extend', () => { 240 | describe('extend', () => { 241 | const f = () => 1; 242 | it('initial', () => { 243 | expect(remoteData.extend(initialRD, f)).toBe(initialRD); 244 | }); 245 | it('pending', () => { 246 | expect(remoteData.extend(pendingRD, f)).toBe(pendingRD); 247 | }); 248 | it('failure', () => { 249 | expect(remoteData.extend(failureRD, f)).toBe(failureRD); 250 | }); 251 | it('pending', () => { 252 | expect(remoteData.extend(successRD, f)).toEqual(success(1)); 253 | }); 254 | }); 255 | }); 256 | describe('Traversable2v', () => { 257 | describe('traverse', () => { 258 | const t = remoteData.traverse(option); 259 | const f = (x: number) => (x >= 2 ? some(x) : none); 260 | it('initial', () => { 261 | expect(t(initialRD, f)).toEqual(some(initialRD)); 262 | }); 263 | it('pending', () => { 264 | expect(t(pendingRD, f)).toEqual(some(pendingRD)); 265 | }); 266 | it('failure', () => { 267 | expect(t(failureRD, f)).toEqual(some(failureRD)); 268 | }); 269 | it('success', () => { 270 | expect(t(success(1), f)).toBe(none); 271 | expect(t(success(3), f)).toEqual(some(success(3))); 272 | }); 273 | }); 274 | }); 275 | describe('Foldable2v', () => { 276 | describe('reduce', () => { 277 | const f = (a: number, b: number) => a + b; 278 | const g = (a: number) => a + 1; 279 | it('initial', () => { 280 | expect(remoteData.reduce(initialRD, 1, f)).toBe(1); 281 | expect(remoteData.foldMap(monoidSum)(initialRD, g)).toBe(0); 282 | expect(remoteData.reduceRight(initialRD, 1, f)).toBe(1); 283 | }); 284 | it('pending', () => { 285 | expect(remoteData.reduce(pendingRD, 1, f)).toBe(1); 286 | expect(remoteData.foldMap(monoidSum)(pendingRD, g)).toBe(0); 287 | expect(remoteData.reduceRight(pendingRD, 1, f)).toBe(1); 288 | }); 289 | it('failure', () => { 290 | expect(remoteData.reduce(failureRD, 1, f)).toBe(1); 291 | expect(remoteData.foldMap(monoidSum)(failureRD, g)).toBe(0); 292 | expect(remoteData.reduceRight(failureRD, 1, f)).toBe(1); 293 | }); 294 | it('success', () => { 295 | expect(remoteData.reduce(successRD, 1, f)).toBe(2); 296 | expect(remoteData.foldMap(monoidSum)(successRD, g)).toBe(2); 297 | expect(remoteData.reduceRight(successRD, 1, f)).toBe(2); 298 | }); 299 | }); 300 | }); 301 | describe('Bifunctor', () => { 302 | describe('bimap', () => { 303 | const f = (l: string): string => `Error: ${l}`; 304 | const g = (a: number): number => a + 1; 305 | it('initial', () => { 306 | expect(remoteData.bimap(initialRD, f, g)).toBe(initial); 307 | expect(remoteData.bimap(initialRD, identity, identity)).toBe(initial); 308 | }); 309 | it('pending', () => { 310 | expect(remoteData.bimap(pendingRD, f, g)).toBe(pending); 311 | expect(remoteData.bimap(pendingRD, identity, identity)).toBe(pending); 312 | }); 313 | it('failure', () => { 314 | expect(remoteData.bimap(failureRD, f, g)).toEqual(remoteData.mapLeft(failureRD, f)); 315 | expect(remoteData.bimap(failureRD, f, g)).toEqual(failure('Error: foo')); 316 | expect(remoteData.bimap(failureRD, identity, identity)).toEqual(failureRD); 317 | }); 318 | it('success', () => { 319 | expect(remoteData.bimap(successRD, f, g)).toEqual(remoteData.map(successRD, g)); 320 | expect(remoteData.bimap(successRD, f, g)).toEqual(success(2)); 321 | expect(remoteData.bimap(successRD, identity, identity)).toEqual(successRD); 322 | }); 323 | }); 324 | describe('mapLeft', () => { 325 | const f2 = () => 1; 326 | it('initial', () => { 327 | expect( 328 | pipe( 329 | initialRD, 330 | mapLeft(f2), 331 | ), 332 | ).toBe(initialRD); 333 | }); 334 | it('pending', () => { 335 | expect( 336 | pipe( 337 | pendingRD, 338 | mapLeft(f2), 339 | ), 340 | ).toBe(pendingRD); 341 | }); 342 | it('failure', () => { 343 | expect( 344 | pipe( 345 | failureRD, 346 | mapLeft(f2), 347 | ), 348 | ).toEqual(failure(1)); 349 | }); 350 | it('success', () => { 351 | expect( 352 | pipe( 353 | successRD, 354 | mapLeft(f2), 355 | ), 356 | ).toBe(successRD); 357 | }); 358 | }); 359 | }); 360 | describe('Alternative', () => { 361 | it('zero', () => { 362 | expect(remoteData.zero()).toBe(initial); 363 | }); 364 | }); 365 | describe('Setoid', () => { 366 | describe('getSetoid', () => { 367 | const equals = getEq(eqString, eqNumber).equals; 368 | it('initial', () => { 369 | expect(equals(initialRD, initialRD)).toBe(true); 370 | expect(equals(initialRD, pendingRD)).toBe(false); 371 | expect(equals(initialRD, failureRD)).toBe(false); 372 | expect(equals(initialRD, successRD)).toBe(false); 373 | }); 374 | it('pending', () => { 375 | expect(equals(pendingRD, initialRD)).toBe(false); 376 | expect(equals(pendingRD, pendingRD)).toBe(true); 377 | expect(equals(pendingRD, failureRD)).toBe(false); 378 | expect(equals(pendingRD, successRD)).toBe(false); 379 | }); 380 | it('failure', () => { 381 | expect(equals(failureRD, initialRD)).toBe(false); 382 | expect(equals(failureRD, pendingRD)).toBe(false); 383 | expect(equals(failureRD, failureRD)).toBe(true); 384 | expect(equals(failure('1'), failure('2'))).toBe(false); 385 | expect(equals(failureRD, successRD)).toBe(false); 386 | }); 387 | it('success', () => { 388 | expect(equals(successRD, initialRD)).toBe(false); 389 | expect(equals(successRD, pendingRD)).toBe(false); 390 | expect(equals(successRD, failureRD)).toBe(false); 391 | expect(equals(successRD, successRD)).toBe(true); 392 | expect(equals(success(1), success(2))).toBe(false); 393 | }); 394 | }); 395 | }); 396 | describe('Ord', () => { 397 | describe('getOrd', () => { 398 | const compare = getOrd(ordString, ordNumber).compare; 399 | it('initial', () => { 400 | expect(compare(initialRD, initialRD)).toBe(0); 401 | expect(compare(initialRD, pendingRD)).toBe(-1); 402 | expect(compare(initialRD, failureRD)).toBe(-1); 403 | expect(compare(initialRD, successRD)).toBe(-1); 404 | }); 405 | it('pending', () => { 406 | expect(compare(pendingRD, initialRD)).toBe(1); 407 | expect(compare(pendingRD, pendingRD)).toBe(0); 408 | expect(compare(pendingRD, failureRD)).toBe(-1); 409 | expect(compare(pendingRD, successRD)).toBe(-1); 410 | }); 411 | it('failure', () => { 412 | expect(compare(failureRD, initialRD)).toBe(1); 413 | expect(compare(failureRD, pendingRD)).toBe(1); 414 | expect(compare(failureRD, failureRD)).toBe(0); 415 | expect(compare(failureRD, successRD)).toBe(-1); 416 | expect(compare(failure('1'), failure('2'))).toBe(-1); 417 | expect(compare(failure('2'), failure('1'))).toBe(1); 418 | }); 419 | it('success', () => { 420 | expect(compare(successRD, initialRD)).toBe(1); 421 | expect(compare(successRD, pendingRD)).toBe(1); 422 | expect(compare(successRD, failureRD)).toBe(1); 423 | expect(compare(successRD, successRD)).toBe(0); 424 | expect(compare(success(1), success(2))).toBe(-1); 425 | expect(compare(success(2), success(1))).toBe(1); 426 | }); 427 | }); 428 | }); 429 | describe('Semigroup', () => { 430 | describe('getSemigroup', () => { 431 | const concat = getSemigroup(semigroupString, semigroupSum).concat; 432 | it('initial', () => { 433 | expect(concat(initialRD, initialRD)).toBe(initialRD); 434 | expect(concat(initialRD, pendingRD)).toBe(pendingRD); 435 | expect(concat(initialRD, failureRD)).toBe(failureRD); 436 | expect(concat(initialRD, successRD)).toBe(successRD); 437 | }); 438 | it('pending', () => { 439 | expect(concat(pendingRD, initialRD)).toBe(pendingRD); 440 | expect(concat(pendingRD, pendingRD)).toBe(pendingRD); 441 | expect(concat(pendingRD, failureRD)).toBe(failureRD); 442 | expect(concat(pendingRD, successRD)).toBe(successRD); 443 | }); 444 | it('failure', () => { 445 | expect(concat(failureRD, initialRD)).toBe(failureRD); 446 | expect(concat(failureRD, pendingRD)).toBe(failureRD); 447 | expect(concat(failure('foo'), failure('bar'))).toEqual( 448 | failure(semigroupString.concat('foo', 'bar')), 449 | ); 450 | expect(concat(failureRD, successRD)).toBe(successRD); 451 | }); 452 | it('success', () => { 453 | expect(concat(successRD, initialRD)).toBe(successRD); 454 | expect(concat(successRD, pendingRD)).toBe(successRD); 455 | expect(concat(successRD, failureRD)).toBe(successRD); 456 | expect(concat(success(1), success(1))).toEqual(success(semigroupSum.concat(1, 1))); 457 | }); 458 | describe('progress', () => { 459 | it('should concat pendings without progress', () => { 460 | expect(concat(pending, pending)).toEqual(pending); 461 | }); 462 | it('should concat pending and progress', () => { 463 | const withProgress: RemoteData = progress({ loaded: 1, total: none }); 464 | expect(concat(pending, withProgress)).toBe(withProgress); 465 | }); 466 | it('should concat progress without total', () => { 467 | const withProgress: RemoteData = progress({ loaded: 1, total: none }); 468 | expect(concat(withProgress, withProgress)).toEqual(progress({ loaded: 2, total: none })); 469 | }); 470 | it('should concat progress without total and progress with total', () => { 471 | const withProgress: RemoteData = progress({ loaded: 1, total: none }); 472 | const withProgressAndTotal: RemoteData = progress({ 473 | loaded: 1, 474 | total: some(2), 475 | }); 476 | expect(concat(withProgress, withProgressAndTotal)).toEqual( 477 | progress({ loaded: 2, total: none }), 478 | ); 479 | }); 480 | it('should combine progresses with total', () => { 481 | const expected = progress({ 482 | loaded: (2 * 10 + 2 * 30) / (40 * 40), 483 | total: some(10 + 30), 484 | }); 485 | expect( 486 | concat(progress({ loaded: 2, total: some(10) }), progress({ loaded: 2, total: some(30) })), 487 | ).toEqual(expected); 488 | }); 489 | }); 490 | }); 491 | }); 492 | describe('Monoid', () => { 493 | it('getMonoid', () => { 494 | const empty = getMonoid(monoidString, monoidSum).empty; 495 | expect(empty).toBe(initial); 496 | }); 497 | }); 498 | describe('Show', () => { 499 | describe('getShow', () => { 500 | const { show } = getShow(showString, showNumber); 501 | it('initial', () => { 502 | expect(show(initialRD)).toBe('initial'); 503 | }); 504 | it('pending', () => { 505 | expect(show(pendingRD)).toBe('pending'); 506 | }); 507 | it('progress', () => { 508 | expect(show(progress({ loaded: 33, total: none }))).toBe('progress({ loaded: 33, total: none })'); 509 | expect(show(progress({ loaded: 33, total: some(123) }))).toBe( 510 | 'progress({ loaded: 33, total: some(123) })', 511 | ); 512 | }); 513 | it('failure', () => { 514 | expect(show(failure('foo'))).toBe('failure("foo")'); 515 | }); 516 | it('success', () => { 517 | expect(show(success(1))).toBe('success(1)'); 518 | }); 519 | }); 520 | }); 521 | }); 522 | describe('top level', () => { 523 | describe('combine', () => { 524 | it('should combine all initials to initial', () => { 525 | expect(combine(initial, initial)).toBe(initial); 526 | }); 527 | it('should combine all pendings to pending', () => { 528 | expect(combine(pending, pending)).toBe(pending); 529 | }); 530 | it('should combine all failures to first failure', () => { 531 | expect(combine(failure('foo'), failure('bar'))).toEqual(failure('foo')); 532 | }); 533 | it('should combine all successes to success of list of values', () => { 534 | expect(combine(success('foo'), success('bar'))).toEqual(success(['foo', 'bar'])); 535 | }); 536 | it('should combine arbitrary non-failure values to first initial', () => { 537 | expect(combine(success(123), success('foo'), pending, initial)).toBe(initial); 538 | expect(combine(initial, pending, success('foo'), success(123))).toBe(initial); 539 | }); 540 | it('should combine arbitrary non-failure & non-initial values to first pending', () => { 541 | expect(combine(success(123), success('foo'), pending)).toBe(pending); 542 | expect(combine(pending, success('foo'), success(123))).toBe(pending); 543 | }); 544 | it('should combine arbitrary values to first failure', () => { 545 | expect(combine(success(123), success('foo'), failure('bar'))).toEqual(failure('bar')); 546 | expect(combine(failure('bar'), success('foo'), success(123))).toEqual(failure('bar')); 547 | }); 548 | describe('progress', () => { 549 | it('should combine pendings without progress', () => { 550 | expect(combine(pending, pending)).toBe(pending); 551 | expect(combine(pending, pending, pending)).toBe(pending); 552 | expect(combine(pending, pending, pending, pending)).toBe(pending); 553 | }); 554 | it('should combine pending and progress', () => { 555 | const withProgress = progress({ loaded: 1, total: none }); 556 | expect(combine(pending, withProgress)).toBe(withProgress); 557 | expect(combine(withProgress, pending)).toBe(withProgress); 558 | }); 559 | it('should combine progress without total', () => { 560 | const withProgress = progress({ loaded: 1, total: none }); 561 | expect(combine(withProgress, withProgress)).toEqual(progress({ loaded: 2, total: none })); 562 | expect(combine(withProgress, withProgress, withProgress)).toEqual( 563 | progress({ loaded: 3, total: none }), 564 | ); 565 | }); 566 | it('should combine progress without total and progress with total', () => { 567 | const withProgress = progress({ loaded: 1, total: none }); 568 | const withProgressAndTotal = progress({ loaded: 1, total: some(2) }); 569 | expect(combine(withProgress, withProgressAndTotal)).toEqual(progress({ loaded: 2, total: none })); 570 | expect(combine(withProgressAndTotal, withProgress)).toEqual(progress({ loaded: 2, total: none })); 571 | }); 572 | it('should combine progresses with total', () => { 573 | const expected = progress({ 574 | loaded: (2 * 10 + 2 * 30) / (40 * 40), 575 | total: some(10 + 30), 576 | }); 577 | expect( 578 | combine(progress({ loaded: 2, total: some(10) }), progress({ loaded: 2, total: some(30) })), 579 | ).toEqual(expected); 580 | expect( 581 | combine(progress({ loaded: 2, total: some(30) }), progress({ loaded: 2, total: some(10) })), 582 | ).toEqual(expected); 583 | }); 584 | }); 585 | }); 586 | describe('fromOption', () => { 587 | const error = new Error('foo'); 588 | it('none', () => { 589 | expect(fromOption(none, () => error)).toEqual(failure(error)); 590 | }); 591 | it('some', () => { 592 | expect(fromOption(some(123), () => error)).toEqual(success(123)); 593 | }); 594 | }); 595 | describe('fromEither', () => { 596 | it('left', () => { 597 | expect(fromEither(left('123'))).toEqual(failure('123')); 598 | }); 599 | it('right', () => { 600 | expect(fromEither(right('123'))).toEqual(success('123')); 601 | }); 602 | }); 603 | describe('fromPredicate', () => { 604 | const factory = fromPredicate((value: boolean) => value, () => '123'); 605 | it('false', () => { 606 | expect(factory(false)).toEqual(failure('123')); 607 | }); 608 | it('true', () => { 609 | expect(factory(true)).toEqual(success(true)); 610 | }); 611 | }); 612 | describe('fromProgressEvent', () => { 613 | const e = new ProgressEvent('test'); 614 | it('lengthComputable === false', () => { 615 | expect(fromProgressEvent({ ...e, loaded: 123 })).toEqual(progress({ loaded: 123, total: none })); 616 | }); 617 | it('lengthComputable === true', () => { 618 | expect(fromProgressEvent({ ...e, loaded: 123, lengthComputable: true, total: 1000 })).toEqual( 619 | progress({ loaded: 123, total: some(1000) }), 620 | ); 621 | }); 622 | }); 623 | describe('getOrElse', () => { 624 | it('initial', () => { 625 | expect(RD.getOrElse(() => 0)(initialRD)).toBe(0); 626 | }); 627 | it('pending', () => { 628 | expect(RD.getOrElse(() => 0)(pendingRD)).toBe(0); 629 | }); 630 | it('failure', () => { 631 | expect(RD.getOrElse(() => 0)(failureRD)).toBe(0); 632 | }); 633 | it('success', () => { 634 | expect(RD.getOrElse(() => 0)(success(1))).toBe(1); 635 | }); 636 | }); 637 | describe('fold', () => { 638 | it('initial', () => { 639 | expect(RD.fold(() => 1, () => 2, () => 3, () => 4)(initialRD)).toBe(1); 640 | }); 641 | it('pending', () => { 642 | expect(RD.fold(() => 1, () => 2, () => 3, () => 4)(pendingRD)).toBe(2); 643 | }); 644 | it('failure', () => { 645 | expect(RD.fold(() => 1, () => 2, () => 3, () => 4)(failureRD)).toBe(3); 646 | }); 647 | it('success', () => { 648 | expect(RD.fold(() => 1, () => 2, () => 3, () => 4)(successRD)).toBe(4); 649 | }); 650 | }); 651 | describe('fold3', () => { 652 | it('initial', () => { 653 | expect(RD.fold3(() => 1, () => 2, () => 3)(initialRD)).toBe(1); 654 | }); 655 | it('pending', () => { 656 | expect(RD.fold3(() => 1, () => 2, () => 3)(pendingRD)).toBe(1); 657 | }); 658 | it('failure', () => { 659 | expect(RD.fold3(() => 1, () => 2, () => 3)(failureRD)).toBe(2); 660 | }); 661 | it('success', () => { 662 | expect(RD.fold3(() => 1, () => 2, () => 3)(successRD)).toBe(3); 663 | }); 664 | }); 665 | describe('isInitial', () => { 666 | it('initial', () => { 667 | expect(isInitial(initialRD)).toBe(true); 668 | }); 669 | it('pending', () => { 670 | expect(isInitial(pendingRD)).toBe(false); 671 | }); 672 | it('failure', () => { 673 | expect(isInitial(failureRD)).toEqual(false); 674 | }); 675 | it('success', () => { 676 | expect(isInitial(successRD)).toBe(false); 677 | }); 678 | }); 679 | describe('isPending', () => { 680 | it('initial', () => { 681 | expect(isPending(initialRD)).toBe(false); 682 | }); 683 | it('pending', () => { 684 | expect(isPending(pendingRD)).toBe(true); 685 | }); 686 | it('failure', () => { 687 | expect(isPending(failureRD)).toEqual(false); 688 | }); 689 | it('success', () => { 690 | expect(isPending(successRD)).toBe(false); 691 | }); 692 | }); 693 | describe('isFailure', () => { 694 | it('initial', () => { 695 | expect(isFailure(initialRD)).toBe(false); 696 | }); 697 | it('pending', () => { 698 | expect(isFailure(pendingRD)).toBe(false); 699 | }); 700 | it('failure', () => { 701 | expect(isFailure(failureRD)).toEqual(true); 702 | if (isFailure(failureRD)) { 703 | expect(failureRD.error).toBeDefined(); 704 | } 705 | }); 706 | it('success', () => { 707 | expect(isFailure(successRD)).toBe(false); 708 | }); 709 | }); 710 | describe('isSuccess', () => { 711 | it('initial', () => { 712 | expect(isSuccess(initialRD)).toBe(false); 713 | }); 714 | it('pending', () => { 715 | expect(isSuccess(pendingRD)).toBe(false); 716 | }); 717 | it('failure', () => { 718 | expect(isSuccess(failureRD)).toEqual(false); 719 | }); 720 | it('success', () => { 721 | expect(isSuccess(successRD)).toBe(true); 722 | if (isSuccess(successRD)) { 723 | expect(successRD.value).toBeDefined(); 724 | } 725 | }); 726 | }); 727 | describe('toOption', () => { 728 | it('initial', () => { 729 | expect(toOption(initialRD)).toBe(none); 730 | }); 731 | it('pending', () => { 732 | expect(toOption(pendingRD)).toBe(none); 733 | }); 734 | it('failure', () => { 735 | expect(toOption(failureRD)).toBe(none); 736 | }); 737 | it('success', () => { 738 | expect(toOption(success(1))).toEqual(some(1)); 739 | }); 740 | }); 741 | describe('toEither', () => { 742 | const initialL = () => 'initial'; 743 | const pendingL = () => 'pending'; 744 | 745 | it('initial', () => { 746 | expect( 747 | pipe( 748 | initialRD, 749 | toEither(initialL, pendingL), 750 | ), 751 | ).toEqual(left('initial')); 752 | }); 753 | it('pending', () => { 754 | expect( 755 | pipe( 756 | pendingRD, 757 | toEither(initialL, pendingL), 758 | ), 759 | ).toEqual(left('pending')); 760 | }); 761 | it('failure', () => { 762 | expect( 763 | pipe( 764 | failureRD, 765 | toEither(initialL, pendingL), 766 | ), 767 | ).toEqual(left('foo')); 768 | }); 769 | it('success', () => { 770 | expect( 771 | pipe( 772 | success(1), 773 | toEither(initialL, pendingL), 774 | ), 775 | ).toEqual(right(1)); 776 | }); 777 | }); 778 | describe('toNullable', () => { 779 | it('initial', () => { 780 | expect(toNullable(initialRD)).toBe(null); 781 | }); 782 | it('pending', () => { 783 | expect(toNullable(pendingRD)).toBe(null); 784 | }); 785 | it('failure', () => { 786 | expect(toNullable(failureRD)).toBe(null); 787 | }); 788 | it('success', () => { 789 | expect(toNullable(success(1))).toEqual(1); 790 | }); 791 | }); 792 | describe('elem', () => { 793 | it('initial', () => { 794 | expect(elem(eqNumber)(1, initialRD)).toBe(false); 795 | }); 796 | it('pending', () => { 797 | expect(elem(eqNumber)(1, pendingRD)).toBe(false); 798 | }); 799 | it('failure', () => { 800 | expect(elem(eqNumber)(1, failureRD)).toBe(false); 801 | }); 802 | it('success', () => { 803 | expect(elem(eqNumber)(1, success(2))).toBe(false); 804 | expect(elem(eqNumber)(1, success(1))).toBe(true); 805 | }); 806 | }); 807 | describe('exists', () => { 808 | const p = (n: number) => n === 1; 809 | it('initial', () => { 810 | expect( 811 | pipe( 812 | initialRD, 813 | exists(p), 814 | ), 815 | ).toBe(false); 816 | }); 817 | it('pending', () => { 818 | expect( 819 | pipe( 820 | pendingRD, 821 | exists(p), 822 | ), 823 | ).toBe(false); 824 | }); 825 | it('failure', () => { 826 | expect( 827 | pipe( 828 | failureRD, 829 | exists(p), 830 | ), 831 | ).toBe(false); 832 | }); 833 | it('success', () => { 834 | expect( 835 | pipe( 836 | success(2), 837 | exists(p), 838 | ), 839 | ).toBe(false); 840 | expect( 841 | pipe( 842 | success(1), 843 | exists(p), 844 | ), 845 | ).toBe(true); 846 | }); 847 | }); 848 | describe('recoverMap', () => { 849 | const f = (error: string) => (error === 'foo' ? some(true) : none); 850 | const isOdd = (n: number) => n % 2 === 0; 851 | it('initial', () => { 852 | expect( 853 | pipe( 854 | initialRD, 855 | recoverMap(f, isOdd), 856 | ), 857 | ).toBe(initialRD); 858 | }); 859 | it('pending', () => { 860 | expect( 861 | pipe( 862 | pendingRD, 863 | recoverMap(f, isOdd), 864 | ), 865 | ).toBe(pendingRD); 866 | }); 867 | it('failure', () => { 868 | expect( 869 | pipe( 870 | failure('foo'), 871 | recoverMap(f, isOdd), 872 | ), 873 | ).toEqual(success(true)); 874 | }); 875 | it('success', () => { 876 | expect( 877 | pipe( 878 | successRD, 879 | recoverMap(f, isOdd), 880 | ), 881 | ).toEqual(success(false)); 882 | }); 883 | }); 884 | describe('instance methods', () => { 885 | describe('recover', () => { 886 | const f = (error: string) => (error === 'foo' ? some(1) : none); 887 | it('initial', () => { 888 | expect( 889 | pipe( 890 | initialRD, 891 | recover(f), 892 | ), 893 | ).toBe(initialRD); 894 | }); 895 | it('pending', () => { 896 | expect( 897 | pipe( 898 | pendingRD, 899 | recover(f), 900 | ), 901 | ).toBe(pendingRD); 902 | }); 903 | it('failure', () => { 904 | expect( 905 | pipe( 906 | failure('foo'), 907 | recover(f), 908 | ), 909 | ).toEqual(success(1)); 910 | }); 911 | it('success', () => { 912 | expect( 913 | pipe( 914 | successRD, 915 | recover(f), 916 | ), 917 | ).toBe(successRD); 918 | }); 919 | }); 920 | }); 921 | }); 922 | 923 | it('types are inferred correctly when chaining', () => { 924 | const fa: RemoteData = RD.success(1); 925 | pipe( 926 | fa, 927 | RD.chain(a => success(a)), 928 | ); 929 | }); 930 | }); 931 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './remote-data'; 2 | export * from './remote-data-io'; 3 | -------------------------------------------------------------------------------- /src/remote-data-io.ts: -------------------------------------------------------------------------------- 1 | import { RemoteData, RemoteFailure, RemoteInitial, RemotePending, RemoteProgress, RemoteSuccess } from './remote-data'; 2 | import { optionFromNullable } from 'io-ts-types/lib/optionFromNullable'; 3 | import { literal, number, type, Type, union } from 'io-ts/lib/index'; 4 | 5 | export type JSONRemoteProgress = { 6 | loaded: number; 7 | total: number | null; 8 | }; 9 | 10 | export type JSONRemotePending = { 11 | _tag: 'RemotePending'; 12 | progress: JSONRemoteProgress | null; 13 | }; 14 | 15 | export type JSONRemoteData = RemoteFailure | RemoteInitial | JSONRemotePending | RemoteSuccess; 16 | 17 | const RemoteInitialCodec: Type = type({ 18 | _tag: literal('RemoteInitial'), 19 | }); 20 | 21 | const RemoteProgressCodec: Type = type({ 22 | loaded: number, 23 | total: optionFromNullable(number), 24 | }); 25 | 26 | const RemotePendingCodec: Type = type({ 27 | _tag: literal('RemotePending'), 28 | progress: optionFromNullable(RemoteProgressCodec), 29 | }); 30 | 31 | export function createRemoteDataFromJSON( 32 | leftCodec: Type, 33 | rightCodec: Type, 34 | ): Type, JSONRemoteData> { 35 | const RemoteFailureCodec = type({ 36 | _tag: literal('RemoteFailure'), 37 | error: leftCodec, 38 | }); 39 | const RemoteSuccessCodec = type({ 40 | _tag: literal('RemoteSuccess'), 41 | value: rightCodec, 42 | }); 43 | return union([RemoteInitialCodec, RemotePendingCodec, RemoteFailureCodec, RemoteSuccessCodec], '_tag'); 44 | } 45 | -------------------------------------------------------------------------------- /src/remote-data-t.ts: -------------------------------------------------------------------------------- 1 | import { 2 | failure, 3 | fold, 4 | progress, 5 | RemoteData, 6 | remoteData, 7 | RemoteProgress, 8 | success, 9 | URI, 10 | initial, 11 | pending, 12 | fromOption, 13 | } from './remote-data'; 14 | 15 | import { 16 | ApplicativeComposition12, 17 | ApplicativeComposition22, 18 | getApplicativeComposition, 19 | ApplicativeCompositionHKT2, 20 | } from 'fp-ts/lib/Applicative'; 21 | import { HKT, Kind, Kind2, Kind3, Kind4, URIS, URIS2, URIS3, URIS4 } from 'fp-ts/lib/HKT'; 22 | import { Monad, Monad1, Monad2, Monad3, Monad4 } from 'fp-ts/lib/Monad'; 23 | import { Option } from 'fp-ts/lib/Option'; 24 | 25 | export type RemoteDataT = HKT>; 26 | 27 | export interface RemoteDataM extends ApplicativeCompositionHKT2 { 28 | readonly chain: (ma: RemoteDataT, f: (a: A) => RemoteDataT) => RemoteDataT; 29 | readonly alt: (fx: RemoteDataT, f: () => RemoteDataT) => RemoteDataT; 30 | readonly bimap: (ma: RemoteDataT, f: (e: E) => N, g: (a: A) => B) => RemoteDataT; 31 | readonly mapLeft: (ma: RemoteDataT, f: (e: E) => N) => RemoteDataT; 32 | readonly fold: ( 33 | ma: RemoteDataT, 34 | onInitial: () => HKT, 35 | onPending: (progress: Option) => HKT, 36 | onFailure: (error: E) => HKT, 37 | onSuccess: (value: A) => HKT, 38 | ) => HKT; 39 | readonly getOrElse: (ma: RemoteDataT, a: () => HKT) => HKT; 40 | readonly orElse: (ma: RemoteDataT, f: () => RemoteDataT) => RemoteDataT; 41 | readonly initial: RemoteDataT; 42 | readonly pending: RemoteDataT; 43 | readonly progress: (mp: RemoteProgress) => RemoteDataT; 44 | readonly progressM: (mp: HKT) => RemoteDataT; 45 | readonly failure: (e: E) => RemoteDataT; 46 | readonly failureM: (me: HKT) => RemoteDataT; 47 | readonly fromM: (ma: HKT) => RemoteDataT; 48 | readonly fromOption: (option: Option, error: () => E) => RemoteDataT; 49 | } 50 | 51 | export type RemoteDataT1 = Kind>; 52 | 53 | export interface RemoteDataM1 extends ApplicativeComposition12 { 54 | readonly chain: (ma: RemoteDataT1, f: (a: A) => RemoteDataT1) => RemoteDataT1; 55 | readonly alt: (fx: RemoteDataT1, f: () => RemoteDataT1) => RemoteDataT1; 56 | readonly bimap: (ma: RemoteDataT1, f: (e: E) => N, g: (a: A) => B) => RemoteDataT1; 57 | readonly mapLeft: (ma: RemoteDataT1, f: (e: E) => N) => RemoteDataT1; 58 | readonly fold: ( 59 | ma: RemoteDataT1, 60 | onInitial: () => Kind, 61 | onPending: (progress: Option) => Kind, 62 | onFailure: (error: E) => Kind, 63 | onSuccess: (value: A) => Kind, 64 | ) => Kind; 65 | readonly getOrElse: (ma: RemoteDataT1, a: () => Kind) => Kind; 66 | readonly orElse: (ma: RemoteDataT1, f: () => RemoteDataT1) => RemoteDataT1; 67 | readonly initial: RemoteDataT1; 68 | readonly pending: RemoteDataT1; 69 | readonly progress: (mp: RemoteProgress) => RemoteDataT1; 70 | readonly progressM: (mp: Kind) => RemoteDataT1; 71 | readonly failure: (e: E) => RemoteDataT1; 72 | readonly failureM: (me: Kind) => RemoteDataT1; 73 | readonly fromM: (ma: Kind) => RemoteDataT1; 74 | readonly fromOption: (option: Option, error: () => E) => RemoteDataT1; 75 | } 76 | 77 | export type RemoteDataT2 = Kind2>; 78 | 79 | export interface RemoteDataM2 extends ApplicativeComposition22 { 80 | readonly chain: ( 81 | ma: RemoteDataT2, 82 | f: (a: A) => RemoteDataT2, 83 | ) => RemoteDataT2; 84 | readonly alt: ( 85 | fx: RemoteDataT2, 86 | f: () => RemoteDataT2, 87 | ) => RemoteDataT2; 88 | readonly bimap: ( 89 | ma: RemoteDataT2, 90 | f: (e: E) => N, 91 | g: (a: A) => B, 92 | ) => RemoteDataT2; 93 | readonly mapLeft: (ma: RemoteDataT2, f: (e: E) => N) => RemoteDataT2; 94 | readonly fold: ( 95 | ma: RemoteDataT2, 96 | onInitial: () => Kind2, 97 | onPending: (progress: Option) => Kind2, 98 | onFailure: (error: E) => Kind2, 99 | onSuccess: (value: A) => Kind2, 100 | ) => Kind2; 101 | readonly getOrElse: (ma: RemoteDataT2, a: () => Kind2) => Kind2; 102 | readonly orElse: ( 103 | ma: RemoteDataT2, 104 | f: () => RemoteDataT2, 105 | ) => RemoteDataT2; 106 | readonly initial: RemoteDataT2; 107 | readonly pending: RemoteDataT2; 108 | readonly progress: (mp: RemoteProgress) => RemoteDataT2; 109 | readonly progressM: (mp: Kind2) => RemoteDataT2; 110 | readonly failure: (e: E) => RemoteDataT2; 111 | readonly failureM: (me: Kind2) => RemoteDataT2; 112 | readonly fromM: (ma: Kind2) => RemoteDataT2; 113 | readonly fromOption: (option: Option, error: () => E) => RemoteDataT2; 114 | } 115 | 116 | export type RemoteDataT3 = Kind3>; 117 | 118 | export interface RemoteDataM3 { 119 | readonly map: (fa: RemoteDataT3, f: (a: A) => B) => RemoteDataT3; 120 | readonly of: (a: A) => RemoteDataT3; 121 | readonly ap: ( 122 | fab: RemoteDataT3 B>, 123 | fa: RemoteDataT3, 124 | ) => RemoteDataT3; 125 | readonly chain: ( 126 | ma: RemoteDataT3, 127 | f: (a: A) => RemoteDataT3, 128 | ) => RemoteDataT3; 129 | readonly alt: ( 130 | fx: RemoteDataT3, 131 | f: () => RemoteDataT3, 132 | ) => RemoteDataT3; 133 | readonly bimap: ( 134 | ma: RemoteDataT3, 135 | f: (e: E) => N, 136 | g: (a: A) => B, 137 | ) => RemoteDataT3; 138 | readonly mapLeft: (ma: RemoteDataT3, f: (e: E) => N) => RemoteDataT3; 139 | readonly fold: ( 140 | ma: RemoteDataT3, 141 | onInitial: () => Kind3, 142 | onPending: (progress: Option) => Kind3, 143 | onFailure: (error: E) => Kind3, 144 | onSuccess: (value: A) => Kind3, 145 | ) => Kind3; 146 | readonly getOrElse: (ma: RemoteDataT3, a: () => Kind3) => Kind3; 147 | readonly orElse: ( 148 | ma: RemoteDataT3, 149 | f: () => RemoteDataT3, 150 | ) => RemoteDataT3; 151 | readonly initial: RemoteDataT3; 152 | readonly pending: RemoteDataT3; 153 | readonly progress: (mp: RemoteProgress) => RemoteDataT3; 154 | readonly progressM: (mp: Kind3) => RemoteDataT3; 155 | readonly failure: (e: E) => RemoteDataT3; 156 | readonly failureM: (me: Kind3) => RemoteDataT3; 157 | readonly fromM: (ma: Kind3) => RemoteDataT3; 158 | readonly fromOption: (option: Option, error: () => E) => RemoteDataT3; 159 | } 160 | 161 | export type RemoteDataT4 = Kind4>; 162 | 163 | export interface RemoteDataM4 { 164 | readonly map: ( 165 | fa: RemoteDataT4, 166 | f: (a: A) => B, 167 | ) => RemoteDataT4; 168 | readonly of: (a: A) => RemoteDataT4; 169 | readonly ap: ( 170 | fab: RemoteDataT4 B>, 171 | fa: RemoteDataT4, 172 | ) => RemoteDataT4; 173 | readonly chain: ( 174 | ma: RemoteDataT4, 175 | f: (a: A) => RemoteDataT4, 176 | ) => RemoteDataT4; 177 | readonly alt: ( 178 | fx: RemoteDataT4, 179 | f: () => RemoteDataT4, 180 | ) => RemoteDataT4; 181 | readonly bimap: ( 182 | ma: RemoteDataT4, 183 | f: (e: E) => N, 184 | g: (a: A) => B, 185 | ) => RemoteDataT4; 186 | readonly mapLeft: ( 187 | ma: RemoteDataT4, 188 | f: (e: E) => N, 189 | ) => RemoteDataT4; 190 | readonly fold: ( 191 | ma: RemoteDataT4, 192 | onInitial: () => Kind4, 193 | onPending: (progress: Option) => Kind4, 194 | onFailure: (error: E) => Kind4, 195 | onSuccess: (value: A) => Kind4, 196 | ) => Kind4; 197 | readonly getOrElse: ( 198 | ma: RemoteDataT4, 199 | a: () => Kind4, 200 | ) => Kind4; 201 | readonly orElse: ( 202 | ma: RemoteDataT4, 203 | f: () => RemoteDataT4, 204 | ) => RemoteDataT4; 205 | readonly initial: RemoteDataT4; 206 | readonly pending: RemoteDataT4; 207 | readonly progress: (mp: RemoteProgress) => RemoteDataT4; 208 | readonly progressM: (mp: Kind4) => RemoteDataT4; 209 | readonly failure: (e: E) => RemoteDataT4; 210 | readonly failureM: (me: Kind4) => RemoteDataT4; 211 | readonly fromM: (ma: Kind4) => RemoteDataT4; 212 | readonly fromOption: (option: Option, error: () => E) => RemoteDataT4; 213 | } 214 | 215 | export function getRemoteDataM(M: Monad4): RemoteDataM4; 216 | export function getRemoteDataM(M: Monad3): RemoteDataM3; 217 | export function getRemoteDataM(M: Monad2): RemoteDataM2; 218 | export function getRemoteDataM(M: Monad1): RemoteDataM1; 219 | export function getRemoteDataM(M: Monad): RemoteDataM; 220 | export function getRemoteDataM(M: Monad): RemoteDataM { 221 | const A = getApplicativeComposition(M, remoteData); 222 | 223 | return { 224 | ...A, 225 | chain: (fa, f) => M.chain(fa, a => (a._tag === 'RemoteSuccess' ? f(a.value) : M.of(a))), 226 | alt: (fa, f) => M.chain(fa, a => (a._tag === 'RemoteSuccess' ? A.of(a.value) : f())), 227 | bimap: (fa, f, g) => M.map(fa, a => remoteData.bimap(a, f, g)), 228 | mapLeft: (ma, f) => M.map(ma, a => remoteData.mapLeft(a, f)), 229 | fold: (ma, onInitial, onPending, onFailure, onSuccess) => 230 | M.chain(ma, fold(onInitial, onPending, onFailure, onSuccess)), 231 | getOrElse: (ma, a) => M.chain(ma, fold(a, a, a, M.of)), 232 | orElse: (ma, f) => M.chain(ma, fold(f, f, f, a => A.of(a))), 233 | initial: M.of(initial), 234 | pending: M.of(pending), 235 | progress: p => M.of(progress(p)), 236 | progressM: mp => M.map(mp, progress), 237 | failure: e => M.of(failure(e)), 238 | failureM: me => M.map(me, failure), 239 | fromM: ma => M.map(ma, success), 240 | fromOption: (a, e) => M.of(fromOption(a, e)), 241 | }; 242 | } 243 | -------------------------------------------------------------------------------- /src/remote-data.ts: -------------------------------------------------------------------------------- 1 | import { constFalse, FunctionN, Lazy, Predicate, identity, constant, Endomorphism } from 'fp-ts/lib/function'; 2 | import { Monad2 } from 'fp-ts/lib/Monad'; 3 | import { Foldable2 } from 'fp-ts/lib/Foldable'; 4 | import { Alt2 } from 'fp-ts/lib/Alt'; 5 | import { Extend2 } from 'fp-ts/lib/Extend'; 6 | import { Traversable2 } from 'fp-ts/lib/Traversable'; 7 | import { Bifunctor2 } from 'fp-ts/lib/Bifunctor'; 8 | import { isNone, isSome, none, Option, some, fold as foldO, getShow as getShowOption } from 'fp-ts/lib/Option'; 9 | import { Either, left, right, fold as foldEither } from 'fp-ts/lib/Either'; 10 | import { Eq } from 'fp-ts/lib/Eq'; 11 | 12 | import { array } from 'fp-ts/lib/Array'; 13 | 14 | import { HKT } from 'fp-ts/lib/HKT'; 15 | import { Applicative } from 'fp-ts/lib/Applicative'; 16 | import { Alternative2 } from 'fp-ts/lib/Alternative'; 17 | import { Ord } from 'fp-ts/lib/Ord'; 18 | import { sign } from 'fp-ts/lib/Ordering'; 19 | import { Semigroup } from 'fp-ts/lib/Semigroup'; 20 | import { Monoid } from 'fp-ts/lib/Monoid'; 21 | import { pipe, pipeable } from 'fp-ts/lib/pipeable'; 22 | import { Show, showNumber } from 'fp-ts/lib/Show'; 23 | 24 | export const URI = 'RemoteData'; 25 | export type URI = typeof URI; 26 | declare module 'fp-ts/lib/HKT' { 27 | interface URItoKind2 { 28 | RemoteData: RemoteData; 29 | } 30 | } 31 | 32 | export type RemoteProgress = { 33 | readonly loaded: number; 34 | readonly total: Option; 35 | }; 36 | 37 | export type RemoteInitial = { 38 | readonly _tag: 'RemoteInitial'; 39 | }; 40 | 41 | export type RemotePending = { 42 | readonly _tag: 'RemotePending'; 43 | readonly progress: Option; 44 | }; 45 | 46 | export type RemoteFailure = { 47 | readonly _tag: 'RemoteFailure'; 48 | readonly error: E; 49 | }; 50 | 51 | export type RemoteSuccess = { 52 | readonly _tag: 'RemoteSuccess'; 53 | readonly value: A; 54 | }; 55 | 56 | /** 57 | * Represents a value of one of four possible types (a disjoint union) 58 | * 59 | * An instance of {@link RemoteData} is either an instance of {@link RemoteInitial}, {@link RemotePending}, {@link RemoteFailure} or {@link RemoteSuccess} 60 | * 61 | * A common use of {@link RemoteData} is as an alternative to `Either` or `Option` supporting initial and pending states (fits best with [RXJS]{@link https://github.com/ReactiveX/rxjs/}). 62 | * 63 | * Note: {@link RemoteInitial}, {@link RemotePending} and {@link RemoteFailure} are commonly called "Left" part in jsDoc. 64 | * 65 | * @see https://medium.com/@gcanti/slaying-a-ui-antipattern-with-flow-5eed0cfb627b 66 | * 67 | */ 68 | export type RemoteData = RemoteInitial | RemotePending | RemoteFailure | RemoteSuccess; 69 | 70 | //constructors 71 | export const failure = (error: E): RemoteData => ({ 72 | _tag: 'RemoteFailure', 73 | error, 74 | }); 75 | export const success = (value: A): RemoteData => ({ 76 | _tag: 'RemoteSuccess', 77 | value, 78 | }); 79 | export const pending: RemoteData = { 80 | _tag: 'RemotePending', 81 | progress: none, 82 | }; 83 | export const progress = (progress: RemoteProgress): RemoteData => ({ 84 | _tag: 'RemotePending', 85 | progress: some(progress), 86 | }); 87 | export const initial: RemoteData = { 88 | _tag: 'RemoteInitial', 89 | }; 90 | 91 | //filters 92 | /** 93 | * Returns true only if {@link RemoteData} is {@link RemoteFailure} 94 | */ 95 | export const isFailure = (data: RemoteData): data is RemoteFailure => data._tag === 'RemoteFailure'; 96 | 97 | /** 98 | * Returns true only if {@link RemoteData} is {@link RemoteSuccess} 99 | */ 100 | export const isSuccess = (data: RemoteData): data is RemoteSuccess => data._tag === 'RemoteSuccess'; 101 | 102 | /** 103 | * Returns true only if {@link RemoteData} is {@link RemotePending} 104 | */ 105 | export const isPending = (data: RemoteData): data is RemotePending => data._tag === 'RemotePending'; 106 | 107 | /** 108 | * Returns true only if {@link RemoteData} is {@link RemoteInitial} 109 | */ 110 | export const isInitial = (data: RemoteData): data is RemoteInitial => data._tag === 'RemoteInitial'; 111 | 112 | /** 113 | * Takes a default value as an argument. 114 | * If this {@link RemoteData} is "Left" part it will return default value. 115 | * If this {@link RemoteData} is {@link RemoteSuccess} it will return it's value ("wrapped" value, not default value) 116 | * 117 | * Note: Default value should be the same type as {@link RemoteData} (internal) value, if you want to pass different type as default, use {@link fold}. 118 | * 119 | * @example 120 | * getOrElse(() => 999)(some(1)) // 1 121 | * getOrElseValue(() => 999)(initial) // 999 122 | */ 123 | export const getOrElse = (f: Lazy) => (ma: RemoteData): A => (isSuccess(ma) ? ma.value : f()); 124 | 125 | /** 126 | * Needed for "unwrap" value from {@link RemoteData} "container". 127 | * It applies a function to each case in the data structure. 128 | * 129 | * @example 130 | * const onInitial = () => "it's initial" 131 | * const onPending = () => "it's pending" 132 | * const onFailure = (err) => "it's failure" 133 | * const onSuccess = (data) => `${data + 1}` 134 | * const f = fold(onInitial, onPending, onFailure, onSuccess) 135 | * 136 | * f(initial) // "it's initial" 137 | * f(pending) // "it's pending" 138 | * f(failure(new Error('error text'))) // "it's failure" 139 | * f(success(21)) // '22' 140 | */ 141 | export const fold = ( 142 | onInitial: () => B, 143 | onPending: (progress: Option) => B, 144 | onFailure: (error: E) => B, 145 | onSuccess: (value: A) => B, 146 | ) => (ma: RemoteData): B => { 147 | switch (ma._tag) { 148 | case 'RemoteInitial': { 149 | return onInitial(); 150 | } 151 | case 'RemotePending': { 152 | return onPending(ma.progress); 153 | } 154 | case 'RemoteFailure': { 155 | return onFailure(ma.error); 156 | } 157 | case 'RemoteSuccess': { 158 | return onSuccess(ma.value); 159 | } 160 | } 161 | }; 162 | 163 | /** 164 | * A more concise way to "unwrap" values from {@link RemoteData} "container". 165 | * It uses fold in its implementation, collapsing `onInitial` and `onPending` on the `onNone` handler. 166 | * When fold's `onInitial` returns, `onNode` is called with `none`. 167 | * 168 | * @example 169 | * const onNone = (progressOption) => "no data to show" 170 | * const onFailure = (err) => "sorry, the request failed" 171 | * const onSuccess = (data) => `result is: ${data + 1}` 172 | * const f = fold(onInitial, onPending, onFailure, onSuccess) 173 | * 174 | * f(initial) // "no data to show" 175 | * f(pending) // "no data to show" 176 | * f(failure(new Error('error text'))) // "sorry, the request failed" 177 | * f(success(21)) // "result is: 22" 178 | */ 179 | export const fold3 = ( 180 | onNone: (progress: Option) => R, 181 | onFailure: (e: E) => R, 182 | onSuccess: (a: A) => R, 183 | ): ((fa: RemoteData) => R) => fold(() => onNone(none), onNone, onFailure, onSuccess); 184 | 185 | /** 186 | * One more way to fold (unwrap) value from {@link RemoteData}. 187 | * `Left` part will return `null`. 188 | * {@link RemoteSuccess} will return value. 189 | * 190 | * For example: 191 | * 192 | * `success(2).toNullable() will return 2` 193 | * 194 | * `initial.toNullable() will return null` 195 | * 196 | * `pending.toNullable() will return null` 197 | * 198 | * `failure(new Error('error text)).toNullable() will return null` 199 | * 200 | */ 201 | export const toNullable = (ma: RemoteData): A | null => (isSuccess(ma) ? ma.value : null); 202 | 203 | export const toUndefined = (ma: RemoteData): A | undefined => (isSuccess(ma) ? ma.value : undefined); 204 | 205 | export function fromOption(option: Option, error: Lazy): RemoteData { 206 | if (isNone(option)) { 207 | return failure(error()); 208 | } else { 209 | return success(option.value); 210 | } 211 | } 212 | 213 | /** 214 | * Convert {@link RemoteData} to {@link Option} 215 | * `Left` part will be converted to {@link None}. 216 | * {@link RemoteSuccess} will be converted to {@link Some}. 217 | * 218 | * @example 219 | * toOption(success(2)) // some(2) 220 | * toOption(initial) // none 221 | * toOption(pending) // none 222 | * toOption(failure(new Error('error text'))) // none 223 | */ 224 | export function toOption(data: RemoteData): Option { 225 | return data._tag === 'RemoteSuccess' ? some(data.value) : none; 226 | } 227 | 228 | /** 229 | * Creates {@link RemoteData} from {@link Either} 230 | */ 231 | export const fromEither: (ea: Either) => RemoteData = foldEither(failure, success as any); 232 | 233 | /** 234 | * Convert {@link RemoteData} to `Either`. 235 | * `Left` part will be converted to `Left`. 236 | * Since {@link RemoteInitial} and {@link RemotePending} do not have `L` values, 237 | * you must provide a value of type `L` that will be used to construct 238 | * the `Left` for those two cases. 239 | * {@link RemoteSuccess} will be converted to `Right`. 240 | * 241 | * @example: 242 | * const f = toEither( 243 | * () => new Error('Data not fetched'), 244 | * () => new Error('Data is fetching') 245 | * ) 246 | * f(success(2)) // right(2) 247 | * f(initial) // right(Error('Data not fetched')) 248 | * f(pending) // right(Error('Data is fetching')) 249 | * f(failure(new Error('error text'))) // right(Error('error text')) 250 | */ 251 | export function toEither(onInitial: () => E, onPending: () => E): (data: RemoteData) => Either { 252 | return data => 253 | pipe( 254 | data, 255 | fold(() => left(onInitial()), () => left(onPending()), left, right), 256 | ); 257 | } 258 | 259 | export function fromPredicate( 260 | predicate: Predicate, 261 | whenFalse: FunctionN<[A], L>, 262 | ): FunctionN<[A], RemoteData> { 263 | return a => (predicate(a) ? success(a) : failure(whenFalse(a))); 264 | } 265 | 266 | /** 267 | * Create {@link RemoteData} from {@link ProgressEvent} 268 | * @param event 269 | */ 270 | export function fromProgressEvent(event: ProgressEvent): RemoteData { 271 | return progress({ 272 | loaded: event.loaded, 273 | total: event.lengthComputable ? some(event.total) : none, 274 | }); 275 | } 276 | 277 | /** 278 | * Compare values and returns `true` if they are identical, otherwise returns `false`. 279 | * `Left` part will return `false`. 280 | * {@link RemoteSuccess} will call {@link Eq.equals}. 281 | * 282 | * If you want to compare {@link RemoteData}'s values better use {@link getEq} or {@link getOrd} helpers. 283 | * 284 | */ 285 | export function elem(E: Eq): (a: A, fa: RemoteData) => boolean { 286 | return (a, fa) => fa._tag === 'RemoteSuccess' && E.equals(a, fa.value); 287 | } 288 | 289 | /** 290 | * Takes a predicate and apply it to {@link RemoteSuccess} value. 291 | * `Left` part will return `false`. 292 | */ 293 | export function exists(p: Predicate): (fa: RemoteData) => boolean { 294 | return fa => fa._tag === 'RemoteSuccess' && p(fa.value); 295 | } 296 | 297 | /** 298 | * Maps this RemoteFailure error into RemoteSuccess if passed function `f` return {@link Some} value, otherwise returns self 299 | */ 300 | export function recover(f: (error: E) => Option): (fa: RemoteData) => RemoteData { 301 | const r: Endomorphism> = recoverMap(f, identity); 302 | return fa => (fa._tag === 'RemoteFailure' ? r(fa) : fa); 303 | } 304 | 305 | /** 306 | * Recovers {@link RemoteFailure} also mapping {@link RemoteSuccess} case 307 | * @see {@link recover} 308 | */ 309 | export function recoverMap( 310 | f: (error: E) => Option, 311 | g: (value: A) => B, 312 | ): (fa: RemoteData) => RemoteData { 313 | return fa => { 314 | switch (fa._tag) { 315 | case 'RemoteInitial': { 316 | return fa; 317 | } 318 | case 'RemotePending': { 319 | return fa; 320 | } 321 | case 'RemoteFailure': { 322 | const b = f(fa.error); 323 | return b._tag === 'Some' ? success(b.value) : fa; 324 | } 325 | case 'RemoteSuccess': { 326 | return success(g(fa.value)); 327 | } 328 | } 329 | }; 330 | } 331 | 332 | const concatPendings = (a: RemotePending, b: RemotePending): RemoteData => { 333 | if (isSome(a.progress) && isSome(b.progress)) { 334 | const progressA = a.progress.value; 335 | const progressB = b.progress.value; 336 | if (isNone(progressA.total) || isNone(progressB.total)) { 337 | return progress({ 338 | loaded: progressA.loaded + progressB.loaded, 339 | total: none, 340 | }); 341 | } 342 | const totalA = progressA.total.value; 343 | const totalB = progressB.total.value; 344 | const total = totalA + totalB; 345 | const loaded = (progressA.loaded * totalA + progressB.loaded * totalB) / (total * total); 346 | return progress({ 347 | loaded, 348 | total: some(total), 349 | }); 350 | } 351 | const noA = isNone(a.progress); 352 | const noB = isNone(b.progress); 353 | if (noA && !noB) { 354 | return b; 355 | } 356 | if (!noA && noB) { 357 | return a; 358 | } 359 | return pending; 360 | }; 361 | 362 | //instance 363 | export const remoteData: Monad2 & 364 | Foldable2 & 365 | Traversable2 & 366 | Bifunctor2 & 367 | Alt2 & 368 | Extend2 & 369 | Alternative2 = { 370 | //HKT 371 | URI, 372 | 373 | //Monad 374 | of: (value: A): RemoteData => success(value), 375 | ap: (fab: RemoteData>, fa: RemoteData): RemoteData => { 376 | switch (fa._tag) { 377 | case 'RemoteInitial': { 378 | return isFailure(fab) ? fab : initial; 379 | } 380 | case 'RemotePending': { 381 | return isPending(fab) ? concatPendings(fa, fab) : isSuccess(fab) ? fa : fab; 382 | } 383 | case 'RemoteFailure': { 384 | return isFailure(fab) ? fab : fa; 385 | } 386 | case 'RemoteSuccess': { 387 | return isSuccess(fab) ? success(fab.value(fa.value)) : fab; 388 | } 389 | } 390 | }, 391 | map: (fa: RemoteData, f: FunctionN<[A], B>): RemoteData => 392 | isSuccess(fa) ? success(f(fa.value)) : fa, 393 | chain: (fa: RemoteData, f: FunctionN<[A], RemoteData>): RemoteData => 394 | isSuccess(fa) ? f(fa.value) : fa, 395 | 396 | //Foldable 397 | reduce: (fa: RemoteData, b: B, f: FunctionN<[B, A], B>): B => 398 | pipe( 399 | fa, 400 | fold(() => b, () => b, () => b, a => f(b, a)), 401 | ), 402 | reduceRight: (fa: RemoteData, b: B, f: (a: A, b: B) => B): B => (isSuccess(fa) ? f(fa.value, b) : b), 403 | foldMap: (M: Monoid) => (fa: RemoteData, f: (a: A) => M): M => 404 | isSuccess(fa) ? f(fa.value) : M.empty, 405 | 406 | //Traversable 407 | traverse: (F: Applicative) => ( 408 | ta: RemoteData, 409 | f: (a: A) => HKT, 410 | ): HKT> => { 411 | if (isSuccess(ta)) { 412 | return F.map(f(ta.value), a => remoteData.of(a)); 413 | } else { 414 | return F.of(ta); 415 | } 416 | }, 417 | sequence: (F: Applicative) => (ta: RemoteData>): HKT> => 418 | remoteData.traverse(F)(ta, identity), 419 | 420 | //Bifunctor 421 | bimap: (fla: RemoteData, f: (u: L) => V, g: (a: A) => B): RemoteData => 422 | pipe( 423 | fla, 424 | fold>( 425 | () => initial, 426 | foldO(() => pending, progress), 427 | e => failure(f(e)), 428 | a => success(g(a)), 429 | ), 430 | ), 431 | mapLeft: (fla: RemoteData, f: (u: L) => V): RemoteData => 432 | fold>( 433 | () => initial, 434 | foldO(() => pending, progress), 435 | e => failure(f(e)), 436 | () => fla as any, 437 | )(fla), 438 | 439 | //Alt 440 | alt: (fx: RemoteData, fy: () => RemoteData): RemoteData => fold(fy, fy, fy, () => fx)(fx), 441 | 442 | //Alternative 443 | zero: (): RemoteData => initial, 444 | 445 | //Extend 446 | extend: (fla: RemoteData, f: FunctionN<[RemoteData], B>): RemoteData => 447 | pipe( 448 | fla, 449 | fold>( 450 | () => initial, 451 | foldO(() => pending, progress), 452 | () => fla as any, 453 | () => success(f(fla)), 454 | ), 455 | ), 456 | }; 457 | 458 | //Eq 459 | export const getEq = (EE: Eq, EA: Eq): Eq> => { 460 | return { 461 | equals: (x, y) => 462 | pipe( 463 | x, 464 | fold( 465 | () => isInitial(y), 466 | () => isPending(y), 467 | xError => 468 | pipe( 469 | y, 470 | fold(constFalse, constFalse, yError => EE.equals(xError, yError), constFalse), 471 | ), 472 | ax => 473 | pipe( 474 | y, 475 | fold(constFalse, constFalse, constFalse, ay => EA.equals(ax, ay)), 476 | ), 477 | ), 478 | ), 479 | }; 480 | }; 481 | 482 | //Ord 483 | const constLt = constant(-1); 484 | const constEq = constant(0); 485 | const constGt = constant(1); 486 | export const getOrd = (OE: Ord, OA: Ord): Ord> => { 487 | return { 488 | ...getEq(OE, OA), 489 | compare: (x, y) => 490 | sign( 491 | pipe( 492 | x, 493 | fold( 494 | () => 495 | pipe( 496 | y, 497 | fold(constEq, constLt, constLt, constLt), 498 | ), 499 | () => 500 | pipe( 501 | y, 502 | fold(constGt, constEq, constLt, constLt), 503 | ), 504 | xError => 505 | pipe( 506 | y, 507 | fold(constGt, constGt, yError => OE.compare(xError, yError), constLt), 508 | ), 509 | xValue => 510 | pipe( 511 | y, 512 | fold(constGt, constGt, constGt, yValue => OA.compare(xValue, yValue)), 513 | ), 514 | ), 515 | ), 516 | ), 517 | }; 518 | }; 519 | 520 | //Semigroup 521 | export const getSemigroup = (SE: Semigroup, SA: Semigroup): Semigroup> => { 522 | return { 523 | concat: (x, y) => { 524 | const constX = constant(x); 525 | const constY = constant(y); 526 | return pipe( 527 | x, 528 | fold( 529 | () => 530 | pipe( 531 | y, 532 | fold(constY, constY, constY, constY), 533 | ), 534 | () => 535 | pipe( 536 | y, 537 | fold(constX, () => concatPendings(x as RemotePending, y as RemotePending), constY, constY), 538 | ), 539 | 540 | xError => 541 | pipe( 542 | y, 543 | fold(constX, constX, yError => failure(SE.concat(xError, yError)), () => y), 544 | ), 545 | xValue => 546 | pipe( 547 | y, 548 | fold(constX, constX, () => x, yValue => success(SA.concat(xValue, yValue))), 549 | ), 550 | ), 551 | ); 552 | }, 553 | }; 554 | }; 555 | 556 | //Monoid 557 | export const getMonoid = (SL: Semigroup, SA: Semigroup): Monoid> => { 558 | return { 559 | ...getSemigroup(SL, SA), 560 | empty: initial, 561 | }; 562 | }; 563 | 564 | const showOptionNumber = getShowOption(showNumber); 565 | //Show 566 | export const getShow = (SE: Show, SA: Show): Show> => ({ 567 | show: fold( 568 | () => 'initial', 569 | foldO( 570 | () => 'pending', 571 | progress => 572 | `progress({ loaded: ${showNumber.show(progress.loaded)}, total: ${showOptionNumber.show( 573 | progress.total, 574 | )} })`, 575 | ), 576 | e => `failure(${SE.show(e)})`, 577 | a => `success(${SA.show(a)})`, 578 | ), 579 | }); 580 | 581 | const { 582 | alt, 583 | ap, 584 | apFirst, 585 | apSecond, 586 | bimap, 587 | chain, 588 | chainFirst, 589 | duplicate, 590 | extend, 591 | flatten, 592 | foldMap, 593 | map, 594 | mapLeft, 595 | reduce, 596 | reduceRight, 597 | } = pipeable(remoteData); 598 | 599 | export { 600 | alt, 601 | ap, 602 | apFirst, 603 | apSecond, 604 | bimap, 605 | chain, 606 | chainFirst, 607 | duplicate, 608 | extend, 609 | flatten, 610 | foldMap, 611 | map, 612 | mapLeft, 613 | reduce, 614 | reduceRight, 615 | }; 616 | 617 | export function combine(a: RemoteData): RemoteData; 618 | export function combine(a: RemoteData, b: RemoteData): RemoteData; 619 | export function combine( 620 | a: RemoteData, 621 | b: RemoteData, 622 | c: RemoteData, 623 | ): RemoteData; 624 | export function combine( 625 | a: RemoteData, 626 | b: RemoteData, 627 | c: RemoteData, 628 | d: RemoteData, 629 | ): RemoteData; 630 | export function combine( 631 | a: RemoteData, 632 | b: RemoteData, 633 | c: RemoteData, 634 | d: RemoteData, 635 | e: RemoteData, 636 | ): RemoteData; 637 | export function combine( 638 | a: RemoteData, 639 | b: RemoteData, 640 | c: RemoteData, 641 | d: RemoteData, 642 | e: RemoteData, 643 | f: RemoteData, 644 | ): RemoteData; 645 | export function combine(...list: RemoteData[]): RemoteData { 646 | if (list.length === 0) { 647 | return remoteData.of([]); 648 | } 649 | return array.sequence(remoteData)(list); 650 | } 651 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "**/__tests__", 5 | "dist", 6 | "es6" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "dist/es6", 5 | "target": "es6", 6 | "module": "es6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "outDir": "dist/lib", 8 | "declaration": true, 9 | "strict": true, 10 | "importHelpers": true, 11 | "pretty": true, 12 | "noUnusedLocals": true, 13 | "noEmitOnError": true, 14 | "esModuleInterop": true 15 | }, 16 | "include": [ 17 | "./src/**/*" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@devexperts/lint/tslint.json", 4 | "tslint-config-prettier" 5 | ], 6 | "rulesDirectory": [ 7 | "tslint-plugin-prettier" 8 | ], 9 | "rules": { 10 | "no-namespace": false, 11 | "prettier": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------