[tink_cli] How to deal with `Cli.exit()` and JS Promises

I’m learning Haxe and trying to create a Node.js command line application using the tink_cli package:

import js.lib.Promise;
import tink.Cli;

class Main {

	static function main()
		Cli.process(Sys.args(), new Main()).handle(Cli.exit);

	@:defaultCommand
	public function run() {
		Promise.resolve(null)
			.then(_ -> Sys.println("Promise called"))
			.catchError(_ -> Sys.println("An error occurred"));
	}
}

The Promise.then() handler is never called because Cli.exit() occurs before.
If I remove the call to Cli.exit(), everything is fine (i.e. "Promise called" is printed):

static function main()
	Cli.process(Sys.args(), new Main());

So what is the proper way to implement the Main.run() method?

You want to actually return your promise in the run method

1 Like

Thanks @benmerckx but unfortunately, this does not change anything. It seems that I need to explicitly wrap the JS promise in a Tinkerbell promise returning an Outcome value:

import js.lib.Promise as JsPromise;
import tink.Cli;
import tink.core.Outcome;
import tink.core.Promise as TinkPromise;

class Main {
  static function main()
    Cli.process(Sys.args(), new Main()).handle(Cli.exit);

  @:defaultCommand
  public function run() {
    final jsPromise = JsPromise.resolve(null).then(_ -> Sys.println("Promise called"));
    return TinkPromise.ofJsPromise(jsPromise).map(_ -> Success("It works!"));
  }
}

Not user friendly :thinking: and it adds too much complexity to the real code (composed of several subcommands and many return paths).

@kevinresol Is this the right way to do it?

You can get rid of some boilerplate by typing the run method so the JS promise can be transformed to a tink one. Note the Noise value here since we can’t reliably work with Void as a value:

import js.lib.Promise as JsPromise;
import tink.Cli;
using tink.CoreApi;

class Main {
  public function new() {}

  static function main()
    Cli.process(Sys.args(), new Main()).handle(Cli.exit);

  @:defaultCommand
  public function run(): Promise<Noise> {
    return JsPromise.resolve(null)
      .then(_ -> Sys.println("Promise called"))
      .catchError(_ -> Sys.println("An error occurred"))
      .then(_ -> Noise);
  }
}

But yeah, mixing JS promises in here will get a little confusing.

If you’re looking for something simple, I personally like hxargs: GitHub - Simn/hxargs: A really small command line parser

It’s used by tools like dox and haxe-formatter:

Thanks again @benmerckx! It’s much better now.

@Gama11 I usually avoid Tinkerbell libraries because they look like an alien to me (I’m too much formatted by many years of JS/PHP coding) but I find that tink_cli is better designed than hxargs. I really like that CLI commands and flags are represented by a class.

A command function in tink_cli should return Promise<Noise>, but the macro will insert a return Promise.NOISE if its return type is Void (this explains why your first example exits before the js promise is done). That returned promise is used to indicate you have done with your tasks.

Generally, I recommend transforming js promise in to tink ones as early as possible, that will let you enjoy Haxe earlier. e.g.

  @:defaultCommand
  public function run(): Promise<Noise> {
    return Promise.ofJsPromise(JsPromise.resolve(null))
      .map(o -> {
         if(o.isSuccess()) Sys.println("Promise called");
         else Sys.println("An error occurred");
         o; // return the outcome itself here so that Cli.exit will exit with the error code in case of an error
         // Noise; // you can also choose to return Noise, if you want the exit code to be always zero
      });
  }
1 Like

Thanks @kevinresol for the clarification.