Showcasing JSAsync: Haxe library for generating native JS async/await functions

If you target JS and you are working with js.lib.Promise you can use this library to escape from callback hell in an ergonomic and type-safe way.

Your haxe code will look like this:

@:jsasync static function fetchText(url : String) {
    return "Text: " + Browser.window.fetch(url).jsawait().text().jsawait();
}

fetchText return type will be inferred to js.lib.Promise<String>

And the js output will be this:

static async fetchText(url) {
    return "Text: " + (await (await window.fetch(url)).text());
}

Haxelib: jsasync (1.2.1)
Repo: GitHub - basro/hx-jsasync: Haxe lib for generating native JS async functions in an ergonomic and type safe way.

I’m using this on my own projects, I hope that someone else will also find it useful.

12 Likes

Nice one! I would mention in the README that you can also statically import JSAsyncTools.await and use it as a “global” function, e.g.

import JSAsyncTools.await;

//...later
var x = await(somePromise);

Maybe also worth mentioning that one can put this into an import.hx file for convenience. I know it’s pretty generic/basic Haxe stuff but a lot of people don’t actually realize you can do this :slight_smile:

Same with JSAsync.func: I would actually import it with import JSAsync.func as async, so I can do async(function(...) { ... }) later. Maybe it’s worth considering renaming func to async for that :slight_smile:

5 Likes

Great suggestions @nadako.
Since when has it been possible to import static fields? I was not aware of this :stuck_out_tongue:

Another thing: What are the chances of Haxe using async and await as keywords in the future? Perhaps I should avoid those names to ensure it wont break if/when the keywords are added to haxe?

I think static imports was there since Haxe 3, so, for quite a long time.

Regarding async/await, the current proposed coroutine design doesn’t reserve these keywords, because it proposes lower level coroutines and the idea is that async and await functions are implemented by the user on top of them, but it’s not set in stone still, so yeah maybe it’s a good idea to avoid those for now.

I’ve implemented your recommendations, I went for JSAsync.jsasync and JSAsyncTools.jsawait to be safe.

Thank you, Mario! Great library!

You are welcome @cambiata, glad that it is useful to others.

Hello all, thank you Mario for your library.
I have question about one showcase, I would be very very happy if somebody can help me with this, please.

I am trying to create dynamically generating html with facilities. The main problem is in for cycle in initFacilities method where I need to wait while for will be done, then continue. In this example I have two facilities (1 and 2). Thank you so much for any help…

public var facilitiesCollector:Array<Dynamic>;

@:jsasync static function fetchText(url : String, ?init:js.html.RequestInit) {
   return js.Browser.window.fetch(url, init).jsawait().text().jsawait();
}

public function new() {
  facilitiesCollector = new Array();
  var facilitiesIds:Array<Int> = [1,2]; // these numbers are unique ids in database
  initFacilities(facilitiesIds); // calling initialisation function
}

 private function initFacilities( ?facilityIds:Array<Int> ):Void
 {
  // TO DO: NEED TO WAIT WHILE FOR WILL BE DONE. HOW??
    for(facilityId in facilityIds) {
        loadFacilityForm(facilityId, facilitiesCollector.length); //  facilitiesCollector.length is used as iterator
    }
 
   trace(facilitiesCollector); // THIS TH POINT WHERE I HAVE STILL EMPTY DATA BECAUSE FOR CYCLUS ABOVE IS NOT WAITING WHILE ALL FUNCTIONS WILL BE DONE. CAN YOU HELP ME WITH THIS HOW WRITE IT BY YOUR LIBRARY?

  // after catching all data (data, template) from server I can generate HTML form 
 }

private function loadFacilityForm(  facilityId:Int, addressesLength:Int = 0 )
{
   fetchText(Configuration.serviceUrl + '/fetch/loadAddressFile',
   {
            method: 'POST',
            mode:    js.html.RequestMode.CORS,
            headers: {
                'Content-Type': 'application/json',  // sent request
                'Accept':       'application/json'   // expected data sent back
            },
            body: haxe.Json.stringify({
                filePath: 'partials/business_address.tpl',
                iterator: cast addressesLength,
                facilityId: cast facilityId
            })
   })
   .then(response -> {
            facilitiesCollector.push(response); // store all data in array
  })
  .catchError(error -> trace(error));
}

I hope you can understand this example, I would be very glad for your any answer.

Thank you so much!

Here’s how I’d solve it:
(I haven’t compiled this code so perhaps there’s errors in it)

public var facilitiesCollector:Array<Dynamic>;

@:jsasync static function fetchText(url : String, ?init:js.html.RequestInit) {
	return js.Browser.window.fetch(url, init).jsawait().text().jsawait();
}

public function new() {
	facilitiesCollector = new Array();
	var facilitiesIds:Array<Int> = [1,2]; // these numbers are unique ids in database
	initAddressDynamicElem(facilitiesIds); // calling initialisation function
}

// Turn this function into an async function
@:jsasync private function initAddressDynamicElem( ?facilityIds:Array<Int> ):Void
{
	// Make a list with all the loadFacilityForm promises:
	var loadList = [for(facilityId in facilityIds) loadFacilityForm(facilityId, facilitiesCollector.length)];

	// Wait for all the promises to finish using Promise.all
	Promise.all(loadList).jsawait();

	trace(facilitiesCollector);
}

// Turn this function into an async function
@:jsasync private function loadFacilityForm( facilityId:Int, addressesLength:Int = 0 )
{
	try {
		var requestOptions = {
			method: 'POST',
			mode:    js.html.RequestMode.CORS,
			headers: {
				'Content-Type': 'application/json',  // sent request
				'Accept':       'application/json'   // expected data sent back
			},
			body: haxe.Json.stringify({
				filePath: 'prequalis/partials/business_address.tpl',
				iterator: cast addressesLength,
				facilityId: cast facilityId
			})
		};
		var response = fetchText(Configuration.serviceUrl + '/ajax/loadAddressFile', requestOptions).jsawait();
		facilitiesCollector.push(response);
	}catch(error) {
		trace(error)
	}
}

Thank you so much for your help. :+1:

Just for complete for compiler there is need to set up this typing:

@:jsasync private function initAddressDynamicElem( ?facilityIds:Array ) : Promise<jsasync.Nothing>
var requestOptions : js.html.RequestInit = {
// ...

Wow, great work!

I was wondering how you managed to add “async” and “await” keywords to the compiled JS and noticed that you used js.Syntax.code function to achieve that.

Do I understand correctly that it is not possible to achieve the same behaviour using @async and @await meta tags?
Because as far as I understand you can’t access (and thus modify) compiled source code in macros, can you?

I would just want that Haxe’s async and await look closer to native JS implementation.
For example:

@async static function fetchText(url : String) {
    return "Text: " + (@await (@await Browser.window.fetch(url)).text());
}

I think it’s possible to do this, it’s just an extra pass that looks for @await and replaces them with calls to jsawait.

However, I think this will break autocompletion. Since I don’t think the macro that replaces @await would run when the language server is queried for completion lists and other typing info inside the function. (I’m not 100% certain so I might be wrong on this)

If haxe ever gets typed metadata, there’s a chance that expression metadata could behave like a macro function, that would help with the autocompletion problem.

Maybe @benmerckx has an opinion on this. He implemented @async / @await for the Tinkerbell Futures/Promises.

See: tink_await package.

Yeah, I’ve also checked that tink_await package. But it does not seem to add async and await keywords to the compiled source code (assuming that I’ve read and understood the source code correctly :slight_smile: ).

tink_await does not use JS promises (it’s a more generic solution, supporting all Haxe targets), but its macro code could eventually be an inspiration for jsasync package.

I just checked tink_await and while autocompletion works better than I expected it would, it’s less than ideal.

For example:

var foo = @await myPromise;
foo.| // Autocompletion seems to work fine here

However when doing the following:

(@await myPromise).| // Haxe thinks the expression is still a promise here, you get the promise members as completion options.

Haxe also doesn’t seem to run the macro when hovering the mouse over a symbol to inspect it’s type. Hovering over the foo variable will show that it’s a Promise instead of just T.

Personally I feel like adding @await metadata support would be a case of form over function, the compilation times get worse and the language server gets confused. I’m quite reluctant to add such a feature.

Anyway, thanks for the answer and the experiments!

Your implementation gave me quite a good food for thought.