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>