I did some research and playing around to figure out how a client-server 
architecture with owlkettle could look like and it works pretty well with using 
threads and channels (As Araq pretty much suggested if I understood him 
correctly).

Below the minimal example that I got to work.

Basically you have 3 threads:

  1. Main Thread (Does nothing, basically asleep the entire time waiting for 
the other 2)
  2. Owlkettle Thread (Renders GUI, listens to channel messages from the 
"server" thread)
  3. Server Thread (Listens to channel messages from the "client" thread and 
responds accordingly)



And 2 channels:

  1. Server => Client
  2. Client => Server



  * A button click triggers a signal
  * String channel message sent to the server via the channel for client => 
server messages (clientChannel)
  * The server - listening for messages on the clientChannel - receives the 
message and sends a response via the channel for server => client messages 
(serverChannel)
  * The client checks for new messages on serverChannel in an "idle"-task 
registered with GTK via `g_idle_add_full`. That task is called by GTK every 
time the GTK thread has "nothing to do" and thus is perfect for checking for 
messages
  * Client sees that a server-message is available and triggers an update of 
the client on the owlkettle side of things
  * owlkettle re-renders its widgets and tries to read the message as part of 
doing so in the `view` method
  * That updates the AppState, which is then reflected in the rendered 
application


    
    
    import owlkettle, owlkettle/[widgetutils, adw]
    import owlkettle/bindings/gtk
    import std/[options, os]
    
    var counter: int = 0
    
    ## Communication
    type ChannelHub = ref object
      serverChannel: Channel[string]
      clientChannel: Channel[string]
    
    proc sendToServer(hub: ChannelHub, msg: string): bool =
      echo "send client => server: ", msg
      hub.clientChannel.trySend(msg)
    
    proc sendToClient(hub: ChannelHub, msg: string): bool =
      echo "send client <= server: ", msg
      hub.serverChannel.trySend(msg)
    
    proc readClientMsg(hub: ChannelHub): Option[string] =
      let response: tuple[dataAvailable: bool, msg: string] = 
hub.clientChannel.tryRecv()
      
      result = if response.dataAvailable:
          echo "read client => server: ", response.repr
          some(response.msg)
        else:
          none(string)
    
    proc readServerMsg(hub: ChannelHub): Option[string] =
      let response: tuple[dataAvailable: bool, msg: string] = 
hub.serverChannel.tryRecv()
      
      result = if response.dataAvailable:
          echo "read client <= server: ", response.repr
          some(response.msg)
        else:
          none(string)
    
    proc hasServerMsg(hub: ChannelHub): bool =
      hub.serverChannel.peek() > 0
    
    
    
    ## Server
    proc setupServer(channels: ChannelHub): Thread[ChannelHub] =
      proc serverLoop(hub: ChannelHub) =
        while true:
          let msg = hub.readClientMsg()
          if msg.isSome():
            discard hub.sendToClient("Received Message " & $counter)
            
            counter.inc
          sleep(0) # Reduces stress on CPU when idle, increase when higher 
latency is acceptable for even better idle efficiency
      
      createThread(result, serverLoop, channels)
    
    
    
    ## Client
    proc addServerListener(app: Viewable, hub: ChannelHub, priority: int = 200) 
# Forward declaration
    
    viewable App:
      hub: ChannelHub
      backendMsg: string = ""
      
      hooks:
        afterBuild:
          addServerListener(state, state.hub)
    
    type ListenerData = object
      hub: ChannelHub
      app: Viewable
    
    proc addServerListener(app: Viewable, hub: ChannelHub, priority: int = 200) 
=
      proc listener(cell: pointer): cbool {.cdecl.} =
        let data = cast[ptr ListenerData](cell)[]
        
        if data.hub.hasServerMsg():
          discard data.app.redraw()
        
        sleep(0) # Reduces stress on CPU when idle, increase when higher 
latency is acceptable for even better idle efficiency
        return true.cbool
      
      let data = allocSharedCell(ListenerData(hub: hub, app: app))
      discard g_idle_add_full(priority.cint, listener, data, nil)
    
    method view(app: AppState): Widget =
      let msg: Option[string] = app.hub.readServerMsg()
      if msg.isSome():
        app.backendMsg = msg.get()
      
      result = gui:
        Window:
          defaultSize = (500, 150)
          title = "Client Server Example"
          
          Box:
            orient = OrientY
            margin = 12
            spacing = 6
            
            Button {.hAlign: AlignCenter, vAlign: AlignCenter.}:
              Label(text = "Click me")
              
              proc clicked() =
                discard app.hub.sendToServer("Frontend message!")
            
            
            Label(text = "Message sent by Backend: ")
            Label(text = app.backendMsg)
    
    proc setupClient(channels: ChannelHub): Thread[ChannelHub] =
      proc startOwlkettle(hub: ChannelHub) =
        adw.brew(gui(App(hub = hub)))
      
      createThread(result, startOwlkettle, channels)
    
    
    ## Main
    proc main() =
      var serverToClientChannel: Channel[string]
      var clientToServerChannel: Channel[string]
      serverToClientChannel.open()
      clientToServerChannel.open()
      
      let hub = ChannelHub(serverChannel: serverToClientChannel, clientChannel: 
clientToServerChannel)
      
      let client = setupClient(hub)
      let server = setupServer(hub)
      joinThreads(server, client)
    
    main()
    
    
    Run

This likely still requires some clean-up, for example I think updating AppState 
could possibly be pushed into the `listener` proc. But as a first stab at it 
(And this being my first proper attempt at doing multi-threading myself in 
nim), that seems reasonable.

Reply via email to