├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── build-test-common.hxml ├── build-test.hxml ├── build.hxml ├── demo ├── Main.hx └── import.hx ├── extraParams.hxml ├── haxelib.json ├── readme.md ├── src └── jsasync │ ├── IJSAsync.hx │ ├── JSAsync.hx │ ├── JSAsyncTools.hx │ ├── Nothing.hx │ └── impl │ ├── Doc.hx │ ├── Helper.hx │ └── Macro.hx └── test ├── Main.hx ├── Util.hx └── import.hx /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | BuildAndTestHaxe41: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Install NodeJS 9 | uses: actions/setup-node@v3 10 | with: 11 | node-version: '16' 12 | 13 | - name: Install Haxe 14 | uses: krdlab/setup-haxe@v1 15 | with: 16 | haxe-version: 4.1.5 17 | 18 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 19 | - uses: actions/checkout@v3 20 | - name: Install libs 21 | run: | 22 | haxelib install utest 23 | haxelib install hxnodejs 24 | haxelib dev jsasync ./ 25 | 26 | - name: Build tests 27 | run: haxe build-test.hxml 28 | 29 | - name: Run tests 30 | run: | 31 | echo "*** ES5 Tests ***" 32 | node ./bin/test-es5.js 33 | echo "*** ES5 Tests No Markers ***" 34 | node ./bin/test-es5-no-marker.js 35 | echo "*** ES6 Tests ***" 36 | node ./bin/test-es6.js 37 | echo "*** ES6 Tests No Markers ***" 38 | node ./bin/test-es6-no-marker.js 39 | 40 | BuildAndTestHaxe42: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Install NodeJS 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: '16' 47 | 48 | - name: Install Haxe 49 | uses: krdlab/setup-haxe@v1 50 | with: 51 | haxe-version: 4.2.5 52 | 53 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 54 | - uses: actions/checkout@v3 55 | 56 | - name: Install libs 57 | run: | 58 | haxelib install utest 59 | haxelib install hxnodejs 60 | haxelib dev jsasync ./ 61 | 62 | - name: Build tests 63 | run: haxe build-test.hxml 64 | 65 | - name: Run tests 66 | run: | 67 | echo "*** ES5 Tests ***" 68 | node ./bin/test-es5.js 69 | echo "*** ES5 Tests No Markers ***" 70 | node ./bin/test-es5-no-marker.js 71 | echo "*** ES6 Tests ***" 72 | node ./bin/test-es6.js 73 | echo "*** ES6 Tests No Markers ***" 74 | node ./bin/test-es6-no-marker.js 75 | 76 | BuildAndTestHaxe43: 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Install NodeJS 80 | uses: actions/setup-node@v3 81 | with: 82 | node-version: '16' 83 | 84 | - name: Install Haxe 85 | uses: krdlab/setup-haxe@v1 86 | with: 87 | haxe-version: 4.3.0 88 | 89 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 90 | - uses: actions/checkout@v3 91 | 92 | - name: Install libs 93 | run: | 94 | haxelib install utest 95 | haxelib install hxnodejs 96 | haxelib dev jsasync ./ 97 | 98 | - name: Build tests 99 | run: haxe build-test.hxml 100 | 101 | - name: Run tests 102 | run: | 103 | echo "*** ES5 Tests ***" 104 | node ./bin/test-es5.js 105 | echo "*** ES5 Tests No Markers ***" 106 | node ./bin/test-es5-no-marker.js 107 | echo "*** ES6 Tests ***" 108 | node ./bin/test-es6.js 109 | echo "*** ES6 Tests No Markers ***" 110 | node ./bin/test-es6-no-marker.js 111 | 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *.code-workspace -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.1 2 | 3 | * Register metadata and defines documentation using init macro instead of haxelib.json, fixes lix installs of jsasync. 4 | 5 | # 1.3.0 6 | 7 | * Add custom metadata and defines documentation (for haxe 4.3.0) 8 | 9 | # 1.2.3 10 | 11 | * Improves performance of the %%async_marker%% post processing pass, useful when dealing with very large js files. 12 | 13 | # 1.2.2 14 | 15 | * Avoids running the macro on functions that contain the display position to improve haxe completion reliability. 16 | 17 | # 1.2.1 18 | 19 | * Fix bug where class methods with arguments that have default values caused the jsasync marker fix pass to break. 20 | 21 | # 1.2.0 22 | 23 | * Use Thenable instead of Promise as argument type of JSAsyncTools.jsawait 24 | 25 | # 1.1.1 26 | 27 | * Fix incorrect js output for module level functions. 28 | 29 | # 1.1.0 30 | 31 | * Rename `JSAsync.func` to `jsasync` and `JSAsyncTools.await` to `jsawait`. Trying to keep away from the names `await` and `async` since they could become keywords in future haxe versions. 32 | * Prefix macro-injected calls to static functions with `std.` to prevent name collisions. 33 | 34 | # 1.0.0 35 | 36 | * Library made public -------------------------------------------------------------------------------- /build-test-common.hxml: -------------------------------------------------------------------------------- 1 | -lib utest 2 | -lib jsasync 3 | -lib hxnodejs 4 | -cp test 5 | -main Main 6 | -D analyzer-optimize 7 | --dce full 8 | --macro jsasync.JSAsync.use() -------------------------------------------------------------------------------- /build-test.hxml: -------------------------------------------------------------------------------- 1 | build-test-common.hxml 2 | -D js-es=6 3 | -js bin/test-es6.js 4 | 5 | --next 6 | 7 | build-test-common.hxml 8 | -D js-es=5 9 | -js bin/test-es5.js 10 | 11 | --next 12 | 13 | build-test-common.hxml 14 | -D jsasync-no-markers 15 | -D js-es=6 16 | -js bin/test-es6-no-marker.js 17 | 18 | --next 19 | 20 | build-test-common.hxml 21 | -D jsasync-no-markers 22 | -D js-es=5 23 | -js bin/test-es5-no-marker.js 24 | 25 | -------------------------------------------------------------------------------- /build.hxml: -------------------------------------------------------------------------------- 1 | extraParams.hxml 2 | -cp src 3 | -main demo.Main 4 | -js bin/main.js 5 | -D js-es=6 6 | -D loop_unroll_max_cost=0 7 | -D analyzer-optimize 8 | 9 | # jsasync-no-markers will output code without any markers and doesn't require modifying the generated js file 10 | # it is probably less fragile but the resulting code is uglier 11 | #-D jsasync-no-markers 12 | 13 | # Enables the usage of @:jsasync on all classes inside the demo package. 14 | --macro jsasync.JSAsync.use("demo") 15 | 16 | # Disables the JSAsync output fixing pass even if markers were generated. 17 | #-D jsasync-no-fix-pass 18 | -------------------------------------------------------------------------------- /demo/Main.hx: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import haxe.Json; 4 | import js.lib.Promise; 5 | import js.Browser; 6 | 7 | #if(haxe >= "4.2") 8 | @:jsasync function moduleLevelFunction() { 9 | Main.timer(1000).jsawait(); 10 | trace("tick"); 11 | Main.timer(1000).jsawait(); 12 | trace("tock"); 13 | } 14 | #end 15 | 16 | class Main { 17 | public static function main() { 18 | mainAsync(); 19 | #if ( haxe >= "4.2" ) 20 | moduleLevelFunction(); 21 | #end 22 | } 23 | 24 | @:jsasync public static function mainAsync() { 25 | count(10).jsawait(); 26 | 27 | var things = Promise.all([ 28 | fetchURLAsText("https://twitter.github.io/"), 29 | fetchURLAsText("https://twitter.github.io/projects"), 30 | fetchURLAsText("https://thisurlwillsurelyfail.com/") 31 | ]).jsawait(); 32 | 33 | trace( things.map(text -> text.length) ); 34 | 35 | var localAsyncFunction = jsasync(function(name:String) { 36 | trace("Running local async function " + name); 37 | timer(1000).jsawait(); 38 | return name + " done!"; 39 | }); 40 | 41 | trace(localAsyncFunction("A").jsawait()); 42 | trace(localAsyncFunction("B").jsawait()); 43 | 44 | var randomWait = jsasync(function() { 45 | if ( Math.random() > 0.5 ) { 46 | return; 47 | } 48 | timer(1000).jsawait(); 49 | }); 50 | 51 | randomWait().jsawait(); 52 | 53 | var a = localAsyncFunction("C"); 54 | var b = localAsyncFunction("D"); 55 | trace( a.jsawait() + " " + b.jsawait() ); 56 | } 57 | 58 | @:jsasync static function count(numbers:Int) { 59 | trace('Counting up to $numbers'); 60 | for ( i in 0...10 ) { 61 | trace(i); 62 | timer(1000).jsawait(); 63 | } 64 | } 65 | 66 | public static function timer(msec:Int) { 67 | return new Promise( function(resolve, reject) { 68 | Browser.window.setTimeout(resolve, msec); 69 | }); 70 | } 71 | 72 | @:jsasync static function fetchURLAsText(url:String) { 73 | try { 74 | trace('Fetching $url'); 75 | var text = Browser.window.fetch(url).jsawait().text().jsawait(); 76 | trace('Fetched $url'); 77 | return text; 78 | } catch(e : Any) { 79 | trace('Failed to fetch $url'); 80 | } 81 | return ""; 82 | } 83 | 84 | @:jsasync static function fetchJSon(url : String) : Promise { 85 | var text = Browser.window.fetch(url).jsawait().text().jsawait(); 86 | return Json.parse(text); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /demo/import.hx: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | #if js 4 | import jsasync.JSAsync.jsasync; 5 | using jsasync.JSAsyncTools; 6 | #end 7 | 8 | -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | --macro jsasync.impl.Macro.init() 2 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsasync", 3 | "url" : "https://github.com/basro/hx-jsasync", 4 | "license": "MIT", 5 | "tags": ["js", "async"], 6 | "description": "Native JavaScript async functions.", 7 | "version": "1.3.1", 8 | "classPath": "src", 9 | "releasenote": "See CHANGELOG", 10 | "contributors": ["marioc"], 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # JSAsync 3 | ![tests status](https://github.com/basro/hx-jsasync/workflows/Tests/badge.svg?branch=master) 4 | 5 | This library lets you create native Javascript async functions in your haxe code in an ergonomic and type safe way. 6 | 7 | For example using JSAsync this haxe code: 8 | ```haxe 9 | @:jsasync static function fetchText(url : String) { 10 | return "Text: " + Browser.window.fetch(url).jsawait().text().jsawait(); 11 | } 12 | ``` 13 | Has return type `js.lib.Promise` 14 | 15 | and compiles to the following js: 16 | ```js 17 | static async fetchText(url) { 18 | return "Text: " + (await (await window.fetch(url)).text()); 19 | } 20 | ``` 21 | 22 | # Installation 23 | 24 | JSAsync requires haxe 4+ 25 | 26 | Install with haxelib 27 | ``` 28 | haxelib install jsasync 29 | ``` 30 | 31 | Then add it to your project's build.hxml 32 | ``` 33 | -lib jsasync 34 | -js main.js 35 | -main my.package.Main 36 | ``` 37 | 38 | # Usage 39 | 40 | JSAsync will convert haxe functions into async functions that return `js.lib.Promise`. Inside of an async function it is possible to await other promises. 41 | 42 | ## Async local functions 43 | 44 | Use the `JSAsync.jsasync` macro to convert a local function declaration into a javascript async function. 45 | 46 | Example: 47 | ```haxe 48 | var myFunc = JSAsync.jsasync(function(val:Int) { 49 | return val; 50 | }); 51 | ``` 52 | 53 | To reduce verbosity consider directly importing the macro: 54 | 55 | ```haxe 56 | import jsasync.JSAsync.jsasync; 57 | 58 | /* ... */ 59 | 60 | var myFunc = jsasync(function(val:Int) { 61 | return val; 62 | }); 63 | ``` 64 | 65 | ## Async methods 66 | 67 | Annotate your class methods with `@:jsasync` to convert them into async functions. 68 | 69 | For the `@:jsasync` metadata to take effect your class needs to be processed with the `JSAsync.build()` type-building macro. 70 | 71 | You can achieve this in three different ways: 72 | 73 | ### Use the @:build compiler metadata 74 | Add `@:build(jsasync.JSAsync.build())` metadata to your class. 75 | ```haxe 76 | @:build(jsasync.JSAsync.build()) 77 | class MyClass { 78 | @:jsasync static function() { 79 | return "hi"; 80 | } 81 | } 82 | ``` 83 | 84 | ### Implement `jsasync.IJSAsync` 85 | For convenience you can implement the interface `jsasync.IJSAsync` which will automatically add the `@:build` metadata to your class. 86 | 87 | ```haxe 88 | import jsasync.IJSAsync; 89 | 90 | class MyClass implements IJSAsync { 91 | @:jsasync static function() { 92 | return "hi"; 93 | } 94 | } 95 | ``` 96 | 97 | ### Use the JSAsync.use initialization macro 98 | 99 | Add `--macro jsasync.JSAsync.use("my.package.name")` to your haxe compiler options to automatically add the `JSAsync.build` macro to all classes inside the specified package name and all of its subpackages. 100 | 101 | ## Await a promise 102 | 103 | Inside of an async function you can await a `js.lib.Promise` using the `JSAsyncTools.jsawait` method: 104 | 105 | ```haxe 106 | import jsasync.IJSAsync; 107 | import jsasync.JSAsyncTools; 108 | 109 | class MyClass implements IJSAsync { 110 | @:jsasync static function example() { 111 | var result = JSAsyncTools.jsawait(js.lib.Promise.resolve(10)); 112 | trace(result); // 10 113 | } 114 | } 115 | ``` 116 | 117 | The `JSAsyncTools.jsawait(promise)` function compiles into `(await promise)` in Javascript. 118 | 119 | JSAsyncTools.jsawait() is meant to be used as a static extension: 120 | 121 | ```haxe 122 | import jsasync.IJSAsync; 123 | using jsasync.JSAsyncTools; 124 | 125 | class MyClass implements IJSAsync { 126 | @:jsasync static function example() { 127 | var result = js.lib.Promise.resolve(10).jsawait(); 128 | trace(result); // 10 129 | } 130 | } 131 | ``` 132 | 133 | Alternatively you can import the jsawait function: 134 | 135 | ```haxe 136 | import jsasync.IJSAsync; 137 | import jsasync.JSAsyncTools.jsawait; 138 | 139 | class MyClass implements IJSAsync { 140 | @:jsasync static function example() { 141 | var result = jsawait(js.lib.Promise.resolve(10)); 142 | trace(result); // 10 143 | } 144 | } 145 | ``` 146 | 147 | You can mix both approaches too. 148 | 149 | ### Using await outside of async functions 150 | 151 | Even though this is an error and will produce invalid JS code at this time JSAsync is not able to detect this. Future versions might improve on this. 152 | 153 | On the bright side, this is a syntax error in javascript so it wont cause silent bugs in your js code. 154 | 155 | ## Typing 156 | 157 | In most cases JSAsync is able to infer the return type of your async function automatically. 158 | 159 | You can explicitly type an async function like you would normally, just remember to use `js.lib.Promise` as your return type. 160 | 161 | Example: 162 | ```haxe 163 | @:jsasync function example() : Promise { 164 | return 10; 165 | } 166 | ``` 167 | 168 | ### Void functions 169 | 170 | If the function you are converting to async has return type Void then JSAsync will use the type `jsasync.Nothing` as the promise return type. 171 | 172 | Example: 173 | ```haxe 174 | @:jsasync function example() : Promise { 175 | trace("This function has no return"); 176 | } 177 | ``` 178 | 179 | ### Returning Promise 180 | 181 | In javascript async functions the code `return myPromise` is equivalent to `return await myPromise`. JSAsync takes this into consideration and will type your functions accordingly. 182 | 183 | Example: 184 | ```haxe 185 | @:jsasync function asyncInt() : Promise { 186 | return 10; 187 | } 188 | 189 | @:jsasync function example(test:Int) : Promise { 190 | switch test { 191 | case 1: return asyncInt(); // Promise gets unwrapped 192 | case 2: return asyncInt().jsawait(); 193 | case 3: return 10.0; 194 | } 195 | } 196 | ``` 197 | 198 | ## Output 199 | 200 | JSAsync will add markers of the form `%%jsasync_marker%%` to your functions. These markers are used to fix the .js file generated by haxe in a post processing pass. 201 | 202 | ### `jsasync-no-markers` 203 | You can use the compiler option `-D jsasync-no-markers` to enable a different approach which doesn't use markers and produces valid code without the need to fix the generated .js file. 204 | 205 | You can use this setting if the marker + fix pass causes problems with your pipeline. 206 | 207 | The generated code without markers is correct but is less compact and possibly a little bit less efficient. 208 | 209 | ### `jsasync-no-fix-pass` 210 | 211 | When using `-D jsasync-no-fix-pass` JSAsync will not run the final file fixing post process even if markers were generated. The output will be invalid js, use this if you wish to inspect how the code looks before the fix pass. 212 | 213 | 214 | ## Recommendations 215 | 216 | ### import.hx 217 | 218 | For extra convenience consider adding an [import.hx file](https://haxe.org/manual/type-system-import-defaults.html) to the root of your project with jsasync imports in it. 219 | 220 | ```haxe 221 | // import.hx 222 | package my.project.root; 223 | 224 | #if js // Only import if target is js to avoid breaking your macros. 225 | import jsasync.JSAsync.jsasync; 226 | import jsasync.JSAsyncTools.jsawait; 227 | using jsasync.JSAsyncTools; 228 | #end 229 | ``` 230 | 231 | This will automatically add these imports to all the modules in your project. In combination with `--macro jsasync.JSAsync.use("my.project.root")` this will significantly reduce verbosity in your project. 232 | 233 | # License 234 | 235 | This project is licensed under the terms of the MIT license. -------------------------------------------------------------------------------- /src/jsasync/IJSAsync.hx: -------------------------------------------------------------------------------- 1 | package jsasync; 2 | 3 | /** Implement this interface to enable @:jsasync on the class methods */ 4 | @:autoBuild(jsasync.JSAsync.build()) 5 | interface IJSAsync {} -------------------------------------------------------------------------------- /src/jsasync/JSAsync.hx: -------------------------------------------------------------------------------- 1 | package jsasync; 2 | 3 | import haxe.macro.Compiler; 4 | import haxe.macro.Expr; 5 | 6 | class JSAsync { 7 | /** Use this macro with function expressions to turn them into async functions. */ 8 | //@:deprecated("JSAsync.func is deprecated, use JSAsync.jsasync instead.") // Haxe issue #9425 9 | @:noCompletion 10 | public static macro function func(e:Expr) { 11 | haxe.macro.Context.warning("JSAsync.func is deprecated, use JSAsync.jsasync instead.", e.pos); 12 | return std.jsasync.impl.Macro.asyncFuncMacro(e); 13 | } 14 | 15 | /** Use this macro with function expressions to turn them into async functions. */ 16 | public static macro function jsasync(e:Expr) { 17 | return std.jsasync.impl.Macro.asyncFuncMacro(e); 18 | } 19 | 20 | #if macro 21 | /** 22 | This macro can be used with `@:build()` compiler metadata to add support for `@:jsasync` 23 | metadata in a class methods. 24 | 25 | Alternatively you can use the IJSAsync interface. 26 | */ 27 | static macro function build() : Array { 28 | return std.jsasync.impl.Macro.build(); 29 | } 30 | 31 | 32 | /** 33 | Call this function with the compiler option `--macro jsasync.JSAsync.use("my.package.name")` to automatically 34 | add the JSAsync.build macro to all classes in the specified package. 35 | */ 36 | static function use(packagePath:String = "") { 37 | Compiler.addGlobalMetadata(packagePath, "@:build(jsasync.JSAsync.build())"); 38 | } 39 | #end 40 | } 41 | -------------------------------------------------------------------------------- /src/jsasync/JSAsyncTools.hx: -------------------------------------------------------------------------------- 1 | package jsasync; 2 | 3 | class JSAsyncTools { 4 | /** Awaits a JS Promise. (only valid inside an async function) */ 5 | @:deprecated("JSAsyncTools.await is deprecated, use JSAsyncTools.jsawait instead") 6 | @:noCompletion 7 | public static extern inline function await(promise: js.lib.Promise): T { 8 | return js.Syntax.code("(await {0})", promise); 9 | } 10 | 11 | /** Awaits a JS Promise. (only valid inside an async function) */ 12 | public static extern inline function jsawait(promise: js.lib.Promise.Thenable): T { 13 | return js.Syntax.code("(await {0})", promise); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/jsasync/Nothing.hx: -------------------------------------------------------------------------------- 1 | package jsasync; 2 | 3 | abstract Nothing(Dynamic) {} 4 | 5 | -------------------------------------------------------------------------------- /src/jsasync/impl/Doc.hx: -------------------------------------------------------------------------------- 1 | package jsasync.impl; 2 | 3 | #if (haxe_ver >= 4.30) 4 | import haxe.macro.Compiler; 5 | 6 | final defines : Array = [ 7 | { 8 | define: "jsasync-no-markers", 9 | doc: "JSAsync will output code without any markers and won't require modifying the generated js file.", 10 | platforms: [Js] 11 | }, 12 | { 13 | define: "jsasync-no-fix-pass", 14 | doc: "JSAsync will not run the final-file fixing post process even if markers were generated. The output will be invalid js, use this if you wish to inspect how the code looks before the fix pass.", 15 | platforms: [Js] 16 | } 17 | ]; 18 | 19 | final metadatas : Array = [ 20 | { 21 | "metadata": ":jsasync", 22 | "doc": "Converts a method into an asynchronous function.", 23 | "targets": [ClassField], 24 | "platforms": [Js] 25 | } 26 | ]; 27 | #end -------------------------------------------------------------------------------- /src/jsasync/impl/Helper.hx: -------------------------------------------------------------------------------- 1 | package jsasync.impl; 2 | 3 | import haxe.macro.Expr; 4 | #if !macro 5 | import js.lib.Promise; 6 | 7 | /** 8 | * In javascript async functions returning a Promise doesn't result in a Promise>, 9 | * instead the promise gets awaited automatically. 10 | * This abstract implicit casts from T or Promise achieving the same result in the Haxe type system. 11 | */ 12 | abstract PromiseReturnValue(Dynamic) from T from Promise {} 13 | #end 14 | 15 | class Helper { 16 | #if !macro 17 | public extern static inline function makeAsync(func: T): T { 18 | return js.Syntax.code("(async {0})", func); 19 | } 20 | 21 | public extern static inline function unwrapPromiseType(value: PromiseReturnValue) : T { 22 | return cast value; 23 | } 24 | #end 25 | 26 | public static macro function method(e:Expr) { 27 | return jsasync.impl.Macro.asyncMethodMacro(e); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/jsasync/impl/Macro.hx: -------------------------------------------------------------------------------- 1 | package jsasync.impl; 2 | 3 | #if macro 4 | import sys.io.File; 5 | import haxe.macro.Compiler; 6 | import haxe.macro.Context; 7 | import haxe.macro.Expr; 8 | import jsasync.impl.Doc; 9 | 10 | using haxe.macro.TypeTools; 11 | using haxe.macro.ComplexTypeTools; 12 | using haxe.macro.ExprTools; 13 | 14 | typedef PromiseTypes = { 15 | promise: ComplexType, 16 | inner: ComplexType 17 | } 18 | 19 | class Macro { 20 | 21 | /** Implementation of JSAsync.jsasync macro */ 22 | static public function asyncFuncMacro(e : Expr) { 23 | if ( Context.containsDisplayPosition(e.pos) ) { 24 | return e; 25 | } 26 | 27 | // Convert FArrow into FAnonymous 28 | switch e.expr { 29 | case EFunction(FArrow, f): 30 | f.expr = macro @:pos(e.pos) return ${f.expr}; 31 | e.expr = EFunction(FAnonymous, f); 32 | default: 33 | } 34 | 35 | switch e.expr { 36 | case EFunction(FAnonymous, f): f.expr = modifyFunctionBody(f, e.pos); 37 | default: Context.error("Argument should be an anonymous function of arrow function", e.pos); 38 | } 39 | 40 | return macro @:pos(e.pos) std.jsasync.impl.Helper.makeAsync(${e}); 41 | } 42 | 43 | /** 44 | Implementation of Helper.method macro 45 | Used by JSAsync build macro to modify method bodies. 46 | */ 47 | static public function asyncMethodMacro(e : Expr) { 48 | return switch e.expr { 49 | default: Context.error("Invalid expression", e.pos); 50 | case EFunction(FAnonymous, f): 51 | var body = modifyFunctionBody(f, e.pos); 52 | if ( useMarkers() ) 53 | macro @:pos(e.pos) { 54 | std.js.Syntax.code("%%async_marker%%"); 55 | ${body}; 56 | } 57 | else 58 | macro @:pos(e.pos) return std.jsasync.impl.Helper.makeAsync(function() ${body})(); 59 | } 60 | } 61 | 62 | /** 63 | Figure out the Promise type a function 64 | */ 65 | static function getPromiseTypes(f:Function, pos: Position) { 66 | var retType = if ( f.ret != null ) { 67 | switch f.ret.toType() { 68 | case null: null; 69 | case t: t.follow().toComplexType(); 70 | } 71 | }else { 72 | var te = 73 | try { 74 | var funcExpr = {expr: EFunction(FAnonymous,f), pos:pos}; 75 | Context.typeExpr(macro {var jsasync_dummy_func = $funcExpr; jsasync_dummy_func;}); 76 | }catch( error : haxe.macro.Error ) { 77 | Context.error("JSASync: " + error.message, error.pos); 78 | } 79 | 80 | switch te.t { 81 | case TFun(_, ret): 82 | var ct = ret.follow().toComplexType(); 83 | ct == null? null : macro : js.lib.Promise<$ct>; 84 | default: null; 85 | } 86 | } 87 | 88 | return switch retType { 89 | case null: Context.error("JSAsync: Function has unknown type", pos); 90 | case TPath({name: "Promise", pack: ["js", "lib"], params: [TPType(innerType)] }): 91 | {promise: retType, inner: innerType}; 92 | default: 93 | Context.error('JSASync: Function should have return type js.lib.Promise\nHave: ${retType.toString()}', pos); 94 | } 95 | } 96 | 97 | /** Implementation of JSAsync.build macro */ 98 | static public function build():Array { 99 | var c = Context.getLocalClass(); 100 | if ( c == null ) return null; 101 | var c = c.get(); 102 | if ( c.meta.has(":jsasync_processed") ) return null; 103 | c.meta.add(":jsasync_processed", [], Context.currentPos()); 104 | 105 | var fields = Context.getBuildFields(); 106 | 107 | for ( field in fields ) { 108 | var m = Lambda.find(field.meta, m -> m.name == ":jsasync"); 109 | if ( m == null ) continue; 110 | 111 | if (Context.containsDisplayPosition(field.pos)) { 112 | continue; 113 | } 114 | 115 | switch field.kind { 116 | case FFun(func): 117 | var funcExpr = {expr: EFunction(FAnonymous, {args: [], ret:func.ret, expr: func.expr} ), pos: field.pos} 118 | func.expr = macro std.jsasync.impl.Helper.method(${funcExpr}); 119 | default: 120 | } 121 | } 122 | 123 | return fields; 124 | } 125 | 126 | static function useMarkers() { 127 | return !Context.defined("jsasync-no-markers"); 128 | } 129 | 130 | static function mapReturns(e: Expr, returnMapper: (re: Null, pos: Position) -> Expr) : Expr { 131 | function mapper(e: Expr) 132 | return switch e.expr { 133 | case EReturn(sub): returnMapper( sub == null? null : sub.map(mapper), e.pos ); 134 | case EFunction(kind, f): e; 135 | default: e.map(mapper); 136 | } 137 | 138 | return mapper(e); 139 | } 140 | 141 | /** Modifies a function body so that all return expressions are of type js.lib.Promise */ 142 | static function wrapReturns(e : Expr, types : PromiseTypes) { 143 | var found = false; 144 | var expr = mapReturns(e, (re, pos) -> 145 | if ( re != null ) { 146 | found = true; 147 | var innerCT = types.inner; 148 | var promiseCT = types.promise; 149 | macro @:pos(pos) return (cast (${re} : $innerCT) : $promiseCT); 150 | } 151 | else 152 | makeReturnNothingExpr(pos, false) 153 | ); 154 | 155 | return { 156 | expr: expr, 157 | found: found 158 | }; 159 | } 160 | 161 | /** Converts a function body to turn it into an async function */ 162 | static function modifyFunctionBody(f:Function, pos: Position) : Expr { 163 | function retMapper(re:Null, pos:Position) { 164 | return 165 | if ( re == null ) macro @:pos(pos) return; 166 | else macro @:pos(pos) return std.jsasync.impl.Helper.unwrapPromiseType(${re}); 167 | } 168 | 169 | var f : Function = { 170 | args: f.args, 171 | ret: f.ret, 172 | expr: mapReturns(f.expr, retMapper), 173 | params: f.params 174 | } 175 | var types = getPromiseTypes(f,pos); 176 | 177 | var wrappedReturns = wrapReturns(f.expr, types); 178 | var exprs = [wrappedReturns.expr]; 179 | if ( !wrappedReturns.found ) exprs.push(makeReturnNothingExpr(pos, true)); 180 | return macro $b{exprs} 181 | } 182 | 183 | static function makeReturnNothingExpr(pos: Position, isLast : Bool) : Expr { 184 | var valueExpr = ( useMarkers() && isLast ) ? "%%async_nothing%%" : ""; 185 | return macro @:pos(pos) return (std.js.Syntax.code($v{valueExpr}) : js.lib.Promise); 186 | } 187 | 188 | public static function init() { 189 | if ( !Context.defined("display") ) { 190 | Context.onAfterGenerate( fixOutputFile ); 191 | } 192 | 193 | #if (haxe_ver >= 4.30) 194 | for (md in metadatas) { 195 | Compiler.registerCustomMetadata(md,"jsasync"); 196 | } 197 | 198 | for (d in defines) { 199 | Compiler.registerCustomDefine(d,"jsasync"); 200 | } 201 | #end 202 | } 203 | 204 | /** 205 | Modifies the js output file. 206 | Adds "async" to functions marked with %%async_marker%% and removes "return %%async_nothing%%;" 207 | */ 208 | static function fixOutputFile() { 209 | if ( Context.defined("jsasync-no-fix-pass") || Context.defined("jsasync-no-markers") || Sys.args().indexOf("--no-output") != -1 ) return; 210 | var output = Compiler.getOutput(); 211 | 212 | /** 213 | * markerRegEx broken down: 214 | * ( # Start of group 1, this will be reinserted on replacement 215 | * (?:function )? # Optionally match a "function " prefix 216 | * (?: 217 | * "(?:[^"\\]|\\.)*" # Match a double quoted string (functions could be quoted strings when their names include special characters) 218 | * | # Or 219 | * \w+ # Match an identifier 220 | * ) 221 | * \s*\([^()]*\) # Match the function params as anything between parenthesis. 222 | * \s*{ # Match the first curly bracket after the function arguments. 223 | * (?:[^{}]|{[^{}]*?})*? # Match everything after the function's opening curly bracket. Is lazy and matches as few 224 | * # characters as possible. 225 | * # It will allow 1 level of nested balanced curly brackets. This is necesary because 226 | * # optional arguments will generate `if ( argument == null ) { argument = defaultValue }` between 227 | * # the function's opening curly bracket and the %%async_marker%% 228 | * ) # End of group 1 229 | * $ # End of this string (where the marker was found) 230 | */ 231 | var functionRegEx = ~/((?:function )?(?:"(?:[^"\\]|\\.)*"|\w+)\s*\([^()]*\)\s*{(?:[^{}]|{[^{}]*?})*?)$/; 232 | var returnNothingRegEx = ~/\s*return %%async_nothing%%;/g; 233 | var outputContent = sys.io.File.getContent(output); 234 | 235 | var splitOutput = outputContent.split("%%async_marker%%;"); 236 | for ( i in 0...(splitOutput.length - 1) ) { 237 | // functionRegEx crashes if searching too long a string 238 | var cutoff = splitOutput[i].length - 3000; 239 | var sub = splitOutput[i].substr(cutoff); 240 | 241 | if ( functionRegEx.match(sub) ) { 242 | sub = functionRegEx.matchedLeft() + "async " + functionRegEx.matched(1); 243 | splitOutput[i] = splitOutput[i].substr(0, cutoff) + sub; 244 | } else throw "Function arguments longer than 3000 characters."; 245 | } 246 | outputContent = splitOutput.join(""); 247 | 248 | outputContent = returnNothingRegEx.replace(outputContent, ""); 249 | File.saveContent(output, outputContent); 250 | } 251 | } 252 | #end -------------------------------------------------------------------------------- /test/Main.hx: -------------------------------------------------------------------------------- 1 | import js.lib.Promise; 2 | import utest.Async; 3 | import utest.ui.Report; 4 | import utest.Test; 5 | import utest.Runner; 6 | import utest.Assert; 7 | 8 | import Util.timer; 9 | 10 | class Main { 11 | public static function main() { 12 | var runner = new Runner(); 13 | runner.addCase(new TestJSAsync()); 14 | Report.create(runner); 15 | runner.run(); 16 | } 17 | } 18 | 19 | class TestJSAsync extends Test { 20 | 21 | function testAnonymousAsyncFunction(async : Async) { 22 | var val = 0; 23 | var func = jsasync(function() { 24 | val = 1; 25 | timer(100).jsawait(); 26 | val = 2; 27 | }); 28 | 29 | var func2 = jsasync(function() { 30 | Assert.equals(0, val); 31 | func().jsawait(); 32 | Assert.equals(2, val); 33 | }); 34 | 35 | var func3 = jsasync(function(returnVal : Int) { 36 | timer(50).jsawait(); 37 | return returnVal + 5; 38 | }); 39 | 40 | var func4 = jsasync(function() { 41 | timer(50).jsawait(); 42 | return func3(5); 43 | }); 44 | 45 | var func5 = jsasync(function() { 46 | var v = func4().jsawait(); 47 | Assert.equals(10, v); 48 | }); 49 | 50 | var run = jsasync(function() { 51 | Promise.all([func5(), func2()]).jsawait(); 52 | async.done(); 53 | }); 54 | 55 | run(); 56 | Assert.equals(1, val); 57 | } 58 | 59 | function testStaticAsyncFunctions(async : Async) { 60 | var run = jsasync(function() { 61 | var n1 = StaticAsyncFunctions.delayNumber(50,1); 62 | var n2 = StaticAsyncFunctions.delayNumber(40,2); 63 | var n3 = StaticAsyncFunctions.quotedFunctionName(); 64 | Assert.equals(3, StaticAsyncFunctions.sumPromises(n1, n2).jsawait()); 65 | Assert.equals(20, n3.jsawait()); 66 | async.done(); 67 | }); 68 | 69 | run(); 70 | } 71 | 72 | function testAsyncMethods(async : Async) { 73 | jsasync(function() { 74 | var obj = new AsyncMethods(); 75 | Assert.equals(0, obj.getVal()); 76 | var p = obj.addIntPromise(StaticAsyncFunctions.delayNumber(20,5)); 77 | Assert.equals(0, obj.getVal()); 78 | Assert.equals(5, p.jsawait()); 79 | Assert.equals(5, obj.getVal()); 80 | Assert.equals("Val: 10", obj.quotedMethod().jsawait()); 81 | Assert.equals(10, obj.getVal()); 82 | Assert.equals(2, obj.asyncMethodWithDefaultValueArgument().jsawait()); 83 | Assert.equals(3, obj.asyncMethodWithDefaultValueArgument(2).jsawait()); 84 | async.done(); 85 | })(); 86 | } 87 | 88 | #if(haxe >= "4.2") 89 | function testModuleLevelAsyncFunctions(async : Async) { 90 | jsasync(function() { 91 | Assert.equals(25, moduleLevelFunction().jsawait()); 92 | async.done(); 93 | })(); 94 | } 95 | #end 96 | } 97 | 98 | class StaticAsyncFunctions { 99 | @:jsasync public static function delayNumber(delayMsec : Int, num : Int ) { 100 | timer(delayMsec).jsawait(); 101 | return num; 102 | } 103 | 104 | @:jsasync public static function sumPromises(a : Promise, b : Promise ) { 105 | return a.jsawait() + b.jsawait(); 106 | } 107 | 108 | @:jsasync @:native("has-qouted-name") public static function quotedFunctionName() { 109 | return delayNumber(10, 10).jsawait() + 10; 110 | } 111 | } 112 | 113 | class AsyncMethods { 114 | var val = 0; 115 | public function new() { 116 | } 117 | 118 | public function getVal() return val; 119 | 120 | @:jsasync public function addIntPromise(num : Promise) { 121 | val += num.jsawait(); 122 | return val; 123 | } 124 | 125 | @:native("quoted-method") @:jsasync public function quotedMethod() { 126 | return "Val: " + addIntPromise(StaticAsyncFunctions.delayNumber(50,5)).jsawait(); 127 | } 128 | 129 | @:jsasync public function asyncMethodWithDefaultValueArgument(num : Int = 1) { 130 | timer(50).jsawait(); 131 | return num + 1; 132 | } 133 | } 134 | 135 | #if(haxe >= "4.2") 136 | @:jsasync function moduleLevelFunction() { 137 | timer(50).jsawait(); 138 | return moduleLevelFunction2(10).jsawait() + 5; 139 | } 140 | 141 | @:jsasync function moduleLevelFunction2(num : Int) { 142 | timer(50).jsawait(); 143 | return num + 10; 144 | } 145 | #end 146 | -------------------------------------------------------------------------------- /test/Util.hx: -------------------------------------------------------------------------------- 1 | import js.lib.Promise; 2 | 3 | 4 | class Util { 5 | public static function timer(msec:Int) { 6 | return new Promise( function(resolve, reject) { 7 | js.Node.setTimeout(resolve, msec); 8 | }); 9 | } 10 | } -------------------------------------------------------------------------------- /test/import.hx: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | #if js 4 | import jsasync.JSAsync.jsasync; 5 | using jsasync.JSAsyncTools; 6 | #end 7 | 8 | --------------------------------------------------------------------------------