I've been experimenting with using Tcl as a local server for a desktop application using a browser as the GUI, and using web sockets. Thus far the web sockets are working and I can send a request from the browser to Tcl, retrieve data from a SQLite database, and pass it back to the browser. And it's not difficult to set up a promise-like process in JS to "wait" for Tcl to respond in a non-blocking manner. Also, I've sent a request id on each message to Tcl such that when it responds JS knows which promise to resolve/reject.
My question concerns working in the other direction; that is, initiating a message from Tcl and waiting for the browser to respond and in a non-blocking manner.
In the code below I'm using a coroutine to store the callback procedure and any relevant arguments at the time the request is sent from Tcl to the browser; and then using the id to have the readable event handler call the correct coroutine and pass it the response. Then the coroutine calls the original callback with its arguments and the response received from the browser.
I tried to simulate a delay in sending two messages through a pipe at different times to see if this would work similar to the asynchronous manner of JS. It appears to provide similar results but, of course, is a very simple example.
Would you please tell me if this is the correct approach to take to accomplish this, and tell me of better ones? Thank you for considering my question.
Would you please also tell me why each coro$id doesn't require a separate coSend$id procedure? It doesn't matter if both messages are sent at the same ms, each calls the correct call back with the correct arguments and response. Thanks.
proc coSend {callBack args} {
set response [yield]
$callBack $response $args
}
proc redo {x} {
if {$x < 11} {
chan puts stdout "x: $x => [expr {$x*100}]ms gone by."
incr x
after 100 redo $x
} else {
set ::forever 1
}
}
proc ReadLine {chan} {
if {[catch {chan gets $chan line} len]} {
chan puts stdout "Error in chan $chan"
return
}
set response [string trim [lassign [split $line] id]]
coro$id $response
}
proc SendMessage {callBack args msg delay} {
coroutine coro[set id [GetMsgID]] coSend $callBack $args
after $delay MimicResponse "$id {$msg}"
}
proc MimicResponse {args} {
chan puts $::wchan $args
}
proc GetMsgID {} {
return [incr ::msg_id]
}
proc CallBack_1 {response args} {
chan puts stdout "$args $response"
}
proc CallBack_2 {response args} {
chan puts stdout "$args $response"
}
lassign [chan pipe] rchan wchan
chan configure $rchan -buffering line -blocking 1 -encoding iso8859-1 -translation crlf
chan configure $wchan -buffering line -blocking 1 -encoding iso8859-1 -translation crlf
chan event $rchan readable [list ReadLine $rchan]
set msg_id 0
redo 1
SendMessage CallBack_1 {arg11 arg12} "This is the response to the first message that should return second." 700
SendMessage CallBack_2 {arg21 arg22 arg23} "This is the response to the second message that should return first." 300
set forever 0
vwait forever
# Results are:
# x: 1 => 100ms gone by.
# x: 2 => 200ms gone by.
# x: 3 => 300ms gone by.
# {{arg21 arg22 arg23}} \{This is the response to the second message that should return first.\}
# x: 4 => 400ms gone by.
# x: 5 => 500ms gone by.
# x: 6 => 600ms gone by.
# x: 7 => 700ms gone by.
# {{arg11 arg12}} \{This is the response to the first message that should return second.\}
# x: 8 => 800ms gone by.
# x: 9 => 900ms gone by.
# x: 10 => 1000ms gone by.
`
CodePudding user response:
In Tcl, coroutines are really little stacks; you can yield
anywhere in the call hierarchy, and do not need to track what's going on. (In some other languages, this would make them "colorless"; you don't need the async/await ceremony to call them.) The things that need to be aware of how they've been called can use info coroutine
to discover if they're running in a coroutine context or not.
The disadvantage of this is that coroutines are comparatively expensive, as they have a (Tcl) stack (but no C stack; Tcl avoids loading that up, and it was work to do that that stack reduction spun out both coroutine
and tailcall
) so you're best associating one of those per significant ongoing operation. An example would be having one for each connection made to a server when there's a non-trivial protocol present, at least until such time as the effort is transferred to another worker thread.
If you're doing much with I/O and coroutines, the coroutine package in Tcllib is recommended for handling the basics of making apparently synchronous code be actually asynchronous.