RLesur / crrri

A Chrome Remote Interface written in R
https://rlesur.github.io/crrri/
Other
157 stars 12 forks source link

Implement an API for event listeners #1

Closed RLesur closed 6 years ago

RLesur commented 6 years ago

Implementing event listeners is not so easy.

Assume that we want to navigate on a website and ensure that the frame is loaded. Since multiple frames can be opened, we want to ensure that the main frame is loaded. In order to achieve this task, we have to send Page.navigate, retrieve the frameId of the response and register a listener on the event Page.frameStoppedLoading for the given frameId.

With the current version of crrri, we have to write this (ugly) code:

library(crrri)
library(promises)
con <- chr_connect()

page <- con %>% Page.enable() # active the events

google_loaded <- page %>%
  Page.navigate(url="http://google.fr") %>% 
  then(onFulfilled = function(value) {promise(function(resolve, reject) {
    ws <- value$cnx$ws
    ws$onMessage(function(event) {
      data <- jsonlite::fromJSON(event$data)
      if (!is.null(data$method) & !is.null(data$params$frameId))
        if (data$method == "Page.frameStoppedLoading" & data$params$frameId == value$result$frameId)
          resolve(list(cnx = value$cnx, result = data$params))
    })
  })})

So, we need to implement high level functions in order to register callbacks on events. For instance, we could do that:

Page.navigate(url="htttp://google.fr") %>%
  Page.frameStoppedLoading(frameId = ~frameId) # or frameId = ~ result$frameId

I wonder what would be the best API?

cderv commented 6 years ago

I need to dig a little onto how all this works really but with only what I currently know and from what you describe I am thinking of something like monitorEvents.

Page.navigate(url="htttp://google.fr") %>%
    monitor(Page.frameStoppedLoading, frameId = .res$frameId)

like a condition to meet. (or Listen() for listening to listeners ?)

However, your approach is also good. I prefer something like

Page.navigate(url="htttp://google.fr") %>%
  Page.frameStoppedLoading(frameId = ~ .res$frameId)

where .res is the results from before like .x in purrr.

Does it make sense ? I think, I need to better understand listeners to think clearly about this.

RLesur commented 6 years ago

Events listeners are useful for two different goals:

  1. Waiting that the event fires to proceed the remaining tasks. I did some tests, and it should work well with pipes and an API like this:
    Page.navigate(url="http://google.fr") %>% 
      Page.frameStoppedLoading(frameId = ~ .res$frameId) %>%
      remaining_task()
  2. Register callback functions. I think this usage should be rare but when I see the Page.screencastFrame event, the user would need to register a callback function to save each frame on the disk.

There's a huge difference between awaiting that one event fires once and calling back a function each time the event fires. For the second usage (callback registering), I think it would be more intuitive to have a function like onEvent('Page.frameStoppedLoading', callback = f) but we also could do the trick with an extra argument (for instance .callback)

Page.frameStoppedLoading(.callback = f)

I see a huge advantage to have R functions like Page.frameStoppedLoading() for events handling: we can build the documentation and have auto completion in RStudio (all the names are awful). Writing the full name of events would be prone to error.

cderv commented 6 years ago

The extra argument is really interesting. Same function for both case. I like Page.frameStoppedLoading(.callback = f)

About autocompletion, we could leverage NSE and use onEvent(Page.frameStoppedLoading, callback = f) function name as symbol in first argument. I think the autocompletion should work. How would it work in a pipe ?

Otherwise, it would be a big change, but a R6 logic would help in those case

Page$frameStoppedLoading$onEvent(callback = f)

The whole API could be written as low level with OOP approach - don't know if it worth keeping it in mind as we loose also a lot.

RLesur commented 6 years ago

I wonder many times too for OOP: it would be much more secure but it is not clear for me how to combine R6 with promises and the websocket connexion. I see one advantage of this first draft (without OOP): we are close to the metal and how the DevTools protocol is used in Javascript.

cderv commented 6 years ago

we are close to the metal and how the DevTools protocol is used in Javascript.

Yes absolutely! I think it is also easier to generate the code and the documentation using FP. Will see how it will do on the run. I'll open an issue for reference that this was a question. We'll close when we are sure.

RLesur commented 6 years ago

I think it's done.