sciter-sdk / go-sciter

Golang bindings of Sciter: the Embeddable HTML/CSS/script engine for modern UI development
https://sciter.com
2.57k stars 268 forks source link

Async in tiscript doesn't work. #153

Closed AllenDang closed 6 years ago

AllenDang commented 6 years ago

My code is below

async function someLongOp() {
  $(#btnGo).state.disabled = true;

  var result = await longOp();

  $(#btnGo).state.disabled = false;
} 

self.post(function() {
  $(#btnGo).on("click", function() {
    someLongOp();
  })
})

It suppose to be a async call, but the main thread is blocked. Am I using await in the right way?

pravic commented 6 years ago

Such things better ask on sciter.com

pravic commented 6 years ago

https://github.com/c-smile/sciter-sdk/issues/83

Wait, you are stating that it does not work only with Go.

Than it belongs here.

AllenDang commented 6 years ago

I actually filed another issue here...

pravic commented 6 years ago

Than it belongs here.

Nah, it doesn't. You just doing it wrong.

First of all, documentation:

The await operator works with promises and only with promises while a view.NativeFunction isn't a promise, of course.

To make your code work use the following:


      async function asyncNative(self) {
        // create a `promise` object with our own callback
        var p = promise(function(resolve, reject) {
          debug: "promise start";
          try {

            // call our native function here
            var res = view.native_func();

            debug: "promise ready";

            // and pass the result back to the `await` waiter
            resolve(res);
          }
          catch(e) {
            debug alert: "promise error" e.message e.data;

            // catch an error and pass it to the `await` caller
            reject(e);
          }

          // nothing to see here, really
          debug: "promise end";
        });

        debug: "request";

        // the following is wrong since native functions aren't promises-like objects:
        // var res = await view.asyncNative();

        // use this instead:
        var res = await p;

        // fullfill the UI:
        debug: "result" res;
        $(output).value = res;

        // and finally restore the button's state back:
        self.state.disabled = false;
      }

And call it:


      $(#nativeasync) << event click {

        // prepare the UI
        this.state.disabled = true;

        // call our async function
        asyncNative(this);

        // the following is wrong because 
        // an async function returns **immediately**;
        // use callback instead
        // this.state.disabled = false;
      }
AllenDang commented 6 years ago

You really save my life here!

AllenDang commented 6 years ago

No, it still doesn't work...

go side

func sleep() {
  time.Sleep(time.Second * 4)
}

tiscript side

async function asyncSleep() {
    var p = promise(function(resolve, reject) {
        try {
            var res = view.sleep();
            resolve(res);
        } catch(e) {
            reject(e);
        }
    });

    $(#btnGo).state.disabled = true;

    var result = await p;

    $(#btnGo).state.disabled = false;
}

When I call asyncSleep() the main thread still be blocked.

pravic commented 6 years ago

Also it can be simplified to:

async function asyncNative() {
  var p = promise(function (on_ok, on_fail) {
    view.async_func(on_ok, on_fail);
  });
  return await p;
}
w.defineFunction("async_func", func(args... *sciter.Value) *sciter.Value {
  on_ok := args[0]
  on_fail := args[1]

  // in form of `script.Invoke(this, "stacktrace", args...)`
  on_ok.Invoke(sciter.NullValue(), "[async_func]", sciter.NewValue("OK"))
})
pravic commented 6 years ago

When I call asyncSleep() the main thread still be blocked.

What do you mean by "main thread"?

AllenDang commented 6 years ago

UI is not responding.

pravic commented 6 years ago

How about doing blocking operations in a goroutine and calling promise when done?

AllenDang commented 6 years ago

I thought about using goroutine and callback when it's done. Side effect is this breaks the async/await pattern sciter has.

pravic commented 6 years ago

Why does this break the pattern?

async function asyncNative() {
  var p = promise();
  view.set_promise(p);

  // await
  var res = await p;

  // use result
  $(output).value = String.printf("ok: %V", res);
}
    @sciter.script(convert=False)
    def set_promise(self, oath):

        def thread_func(oath):
            import time
            time.sleep(2)
            oath(True, ["OK"])

        import threading
        t = threading.Thread(target=thread_func, args=(oath,))
        t.start()
    pass

It is in Python but you've got the point: I create a promise, pass it to the native code which creates a new thread and does some sleepy work. Eventually the promise is fulfilled by result.

Note the array in oath(True, params) because (from the docs):

call it as prom(true, params). Where the params is an array of values that will be applied to onFulfilled callback functions registered by .then() method.

AllenDang commented 6 years ago

En, you got the point. I think you are right.

AllenDang commented 6 years ago

It seems I cannot invoke the promise object at go side.

tiscript

var p = promise();
view.longOp(p);
var result = await p;

go side

func waitHere(p *sciter.Value) {
  time.Sleep(time.Second * 5)
  p.Invoke(sciter.NullValue(), "[native func wait]", sciter.NewValue(true), sciter.NewValue())
}

func longOp(...) {
  promise := args[0]
  go waitHere(promise)
}

Turns out promise in go side is not a function. Error message is HV_INCOMPATIBLE_TYPE: ValueInvoke.

I think I'm very close to the solution.... What I do wrong here?

pravic commented 6 years ago

Well, it was tricky.

The problem was in a premature optimization in a way how go-sciter deals with scripting arguments. They are pointers (not values), that's why you have garbage in goroutine instead of promise (because longOp goes out of scope and its arguments are freed by Sciter internally - it knows nothing that goroutine holds some pointer still).

Workaround (until I fix it) would be the following:

func longOp(...) {
  promise := args[0] // `promise` here just a pointer
  go waitHere(promise.Clone()) // make a real Value from it
}
AllenDang commented 6 years ago

Aha! The workaround works. I will use it now and looking forward to your fix. :)

pravic commented 6 years ago

apparently, there is no online equivalent of the sciter-sdk/doc/content/script/language/promise.htm

I've just found it: https://sciter.com/docs/content/sciter/promise.htm It is inside the DOM classes section unlike in offline docs where it is suited in Sciter Script's section.

jqqjj commented 3 years ago

I have the same problem. Could you show your full example codes? @AllenDang

My problem is here. #254

Thanks.