Webview + Promises + Thread
Posted: Thu Jun 20, 2024 3:05 pm
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
webview.html
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())
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>