Page 1 of 1

Webview + Promises + Thread

Posted: Thu Jun 20, 2024 3:05 pm
by tored
Example of calling worker thread from JavaScript in webview by using promises.

You can of course also skip the thread part and do the work in the main thread by calling correct function in PendingPromise()

webview.pb

Code: Select all

EnableExplicit

CompilerIf Not #PB_Compiler_Thread
  CompilerError "Enable thread safe compiler option"
CompilerEndIf

Enumeration
  #WINDOW
  #WEBVIEW
EndEnumeration

; Use message loop to dispatch Promise events
Enumeration Event #PB_Event_FirstCustomValue
  #PROMISE_PENDING
  #PROMISE_FULFILLED
  #PROMISE_REJECTED
EndEnumeration

Global promiseCounter.i = 0

Structure Promise
  promiseId.i
  name.s
  args.s
  response.s
EndStructure  

; Promise List Allocator, has all promises
Global NewList promiseMem.Promise()
; Available Promises that can be used for dispatch
Global NewList *promisePool.Promise()

Global promiseQueueLock = CreateMutex()
If Not promiseQueueLock
  Debug "Failed creating mutex"
  End 1
EndIf
Global promiseQueueSignal = CreateSemaphore()
If Not promiseQueueSignal
  Debug "Failed creating semaphore"
  End 1
EndIf
; Promises waiting for processing, requires locking
Global NewList *promiseQueue.Promise()
; requires locking
Global shutdown = #False

Macro Quote(string)
  #DQUOTE$ + string + #DQUOTE$
EndMacro  

Prototype PromiseFunction(*promise.Promise)

; Procedure exposed to JavaScript running in seperate thread
Runtime Procedure JsFoo(*promise.Promise)
  ; do some heavy work
  Debug *promise\args
  ; Return json
  *promise\response = "[23, 17]"
  ; Return True to resolve promise, False for reject
  ProcedureReturn #True
EndProcedure

; Our thread worker, waits for promise signal to process promises from *promiseQueue()
Procedure Worker(*null)
  Protected *promise.Promise, name.s, fn.PromiseFunction
  
  Repeat
    LockMutex(promiseQueueLock)
    If shutdown = #True
      UnlockMutex(promiseQueueLock) 
      Break
    EndIf
    
    If ListSize(*promiseQueue())
      FirstElement(*promiseQueue())
      *promise = *promiseQueue()
      DeleteElement(*promiseQueue())
      UnlockMutex(promiseQueueLock)
      
      ; or if you dont like runtime you can use a hardcoded Select instead
      name = "Js" + UCase(Left(*promise\name, 1)) + Mid(*promise\name, 2) + "()"
      fn = GetRuntimeInteger(name)
      If fn
        If fn(*promise)
          PostEvent(#PROMISE_FULFILLED, #WINDOW, #WEBVIEW, #Null, *promise)
        Else
          PostEvent(#PROMISE_REJECTED, #WINDOW, #WEBVIEW, #Null, *promise)
        EndIf
      Else
        *promise\response = Quote("No such function called " + *promise\name)
        PostEvent(#PROMISE_REJECTED, #WINDOW, #WEBVIEW, #Null, *promise)
      EndIf    
    Else  
      UnlockMutex(promiseQueueLock)  
    EndIf
    WaitSemaphore(promiseQueueSignal)
  ForEver
EndProcedure

Global worker = CreateThread(@Worker(), #Null)
If Not worker
  Debug "Failed creating thread"
  End 1
EndIf

Procedure AcquirePromise()
  Protected *promise.Promise
  
  If Not ListSize(*promisePool())
    *promise = AddElement(promiseMem())
    If Not *promise
      ProcedureReturn #Null
    EndIf    
  Else   
    FirstElement(*promisePool())
    *promise = *promisePool()
    DeleteElement(*promisePool())
  EndIf
  ProcedureReturn *promise
EndProcedure

Procedure ReleasePromise(*promise.Promise)
  *promise\promiseId = #Null
  *promise\name = #Null$
  *promise\args = #Null$
  *promise\response = #Null$
  LastElement(*promisePool())
  AddElement(*promisePool())
  *promisePool() = *promise
EndProcedure

Procedure PendingPromise()
  Protected *promise.Promise = EventData()
  LockMutex(promiseQueueLock)
  LastElement(*promiseQueue())
  AddElement(*promiseQueue())
  *promiseQueue() = *promise
  UnlockMutex(promiseQueueLock)
  SignalSemaphore(promiseQueueSignal)
EndProcedure
BindEvent(#PROMISE_PENDING, @PendingPromise(), #WINDOW, #WEBVIEW)

Procedure FulfilledPromise()
  Protected *promise.Promise = EventData()    
  WebViewExecuteScript(#WEBVIEW, "bridge.fulFillPromise(" + Str(*promise\promiseId) + ", " + *promise\response + ")")
  ReleasePromise(*promise)
EndProcedure
BindEvent(#PROMISE_FULFILLED, @FulfilledPromise(), #WINDOW, #WEBVIEW)

Procedure RejectedPromise()
  Protected *promise.Promise = EventData() 
  WebViewExecuteScript(#WEBVIEW, "bridge.rejectPromise(" + Str(*promise\promiseId) + ", " + *promise\response + ")")
  ReleasePromise(*promise)
EndProcedure
BindEvent(#PROMISE_REJECTED, @RejectedPromise(), #WINDOW, #WEBVIEW)

Procedure JsNewPromise(json.s)
  Protected *promise.Promise, name.s, args.s, pos, len, delim.s = ","
  
  ; hackish way to shift first element of JSON array
  pos = FindString(json, delim)
  If Not pos
    ProcedureReturn #Null
  EndIf  
  len = Len(delim)
  name = Left(json, pos - 1)
  name = LTrim(name, "[")
  name = Trim(name, #DQUOTE$)
  args = Mid(json, pos + len)
  args = "[" + args  
  
  *promise = AcquirePromise()
  If Not *promise
    ProcedureReturn #Null
  EndIf
  promiseCounter + 1
  *promise\promiseId = promiseCounter
  *promise\name = name
  *promise\args = args
  PostEvent(#PROMISE_PENDING, #WINDOW, #WEBVIEW, #Null, *promise)
  ProcedureReturn UTF8(Str(promiseCounter))
EndProcedure

Procedure JsDebug(json.s)
  If Not ParseJSON(0, json)
    Debug "Error parsing JSON"
    ProcedureReturn
  EndIf
  Debug ComposeJSON(0, #PB_JSON_PrettyPrint)
  FreeJSON(0)
EndProcedure

OpenWindow(#WINDOW, 100, 100, 400, 400, "Webview example", #PB_Window_SystemMenu | #PB_Window_Invisible)

WebViewGadget(#WEBVIEW, 0, 0, 400, 400)
SetGadgetText(#WEBVIEW, "file://" + GetCurrentDirectory() + "webview.html")

BindWebViewCallback(#WEBVIEW, "debug", @JsDebug())
BindWebViewCallback(#WEBVIEW, "newPromise", @JsNewPromise())

HideWindow(#WINDOW, #False)

Define event
Repeat 
  event = WaitWindowEvent()
Until event = #PB_Event_CloseWindow

; Shutdown
LockMutex(promiseQueueLock)
shutdown = #True
UnlockMutex(promiseQueueLock)
SignalSemaphore(promiseQueueSignal)

Define time = ElapsedMilliseconds()
Repeat
  If Not IsThread(worker)
    Break
  EndIf
  WaitThread(worker, 10)
  If ElapsedMilliseconds() - time > 50
    KillThread(worker)
  EndIf
ForEver

FreeMutex(promiseQueueLock)
FreeSemaphore(promiseQueueSignal)
FreeList(*promiseQueue())
FreeList(*promisePool())
FreeList(promiseMem())
webview.html

Code: Select all

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <button id="action">Click me</button>
    <script>
      // Wrap Promise to be able to call reject or resolve from the outside
      class Deferred {
        constructor() {
          this.promise = new Promise((resolve, reject)=> {
            this.reject = reject
            this.resolve = resolve
          })
        }
      }

      // Uses Proxy to detect any method on Bridge and than dispatch that to backend
      class Bridge {
        /** @type {Object.<number, Deferred>} */
        deferreds = [];

        constructor() {
          return new Proxy(this, {
            get: function (bridge, field) {
              if (field in bridge) return bridge[field];
              return function (...args) {
                const deferred = new Deferred();
                newPromise(field, ...args).then((promiseId) => {
                  bridge.deferreds[promiseId] = deferred;
                }, (error) => {
                  deferred.reject("Failed creating new promise");
                });
                return deferred.promise;
              }
            }
          });
        }

        fulFillPromise(promiseId, data) {
          if (!this.deferreds[promiseId]) {
            debug("No such promiseId " + promiseId);
            return;
          }
          const deferred = this.deferreds[promiseId];
          delete this.deferreds[promiseId];
          deferred.resolve(data);
        }

        rejectPromise(promiseId, data) {
          if (!this.deferreds[promiseId]) {
            debug("No such promiseId " + promiseId);
            return;
          }
          const deferred = this.deferreds[promiseId];
          delete this.deferreds[promiseId];
          deferred.reject(data);
        }
      }
      const bridge = new Bridge();

      /**
       * @param {Error} error
       * @returns {string[]}
       */
      function getStackTrace(error) {
        const stack = error.stack || '';
        return stack
                .split('\n')
                .map(function (line) {
                  return line.trim();
                })
                .filter(function (line) {
                  return !!line;
                });
      }

      // log JavaScript errors
      window.addEventListener('error', (e) => {
        const trace = getStackTrace(e.error);
        debug(...trace);
      });

      document.getElementById("action").addEventListener("click", (e) => {
        // call method foo with arguments
        bridge.foo(1, 2).then((result) =>{
            debug(...result);
        }, (error) => {
          debug("Error: " + error);
        });
      });
    </script>
  </body>
</html>

Re: Webview + Promises + Thread

Posted: Wed Jun 26, 2024 9:42 pm
by idle
Thanks looks useful.