> I'm creating an app that will have a few different 'worker' threads going at once, but only one can be 'active' at a time (i.e. the user can interact). How can I keep those running but change which one is 'active', including destroying a thread and observer if it is not 'active' and idle for too long? This sounds like a good use case for a combination of reactive values, observers, and invalidation. Here's an approach: ``` server <- function(input, output, session) { # Track active worker and all running workers active_worker <- reactiveVal(NULL) worker_states <- reactiveValues() last_active_times <- reactiveValues() # Function to create a new worker create_worker <- function(worker_id) { # Create the worker thread worker_states[[worker_id]] <- reactiveVal("running") last_active_times[[worker_id]] <- Sys.time() # Create observer for this worker observer <- observeEvent(input[[paste0("worker_", worker_id, "_input")]], { req(active_worker() == worker_id) # Only process if this is the active worker # Update last active time last_active_times[[worker_id]] <- Sys.time() # Worker-specific logic here # ... }) # Create cleanup observer that checks for idle timeout cleanup_observer <- observe({ invalidateLater(60000) # Check every minute if(active_worker() != worker_id) { # If not active idle_time <- difftime(Sys.time(), last_active_times[[worker_id]], units="mins") if(idle_time > 30) { # If idle for more than 30 minutes # Cleanup worker observer$destroy() cleanup_observer$destroy() worker_states[[worker_id]] <- NULL last_active_times[[worker_id]] <- NULL } } }) return(list( worker_id = worker_id, observer = observer, cleanup = cleanup_observer )) } # Handle worker switching observeEvent(input$switch_worker, { new_worker_id <- input$selected_worker # Create new worker if it doesn't exist if(is.null(worker_states[[new_worker_id]])) { create_worker(new_worker_id) } # Update active worker active_worker(new_worker_id) }) # Output to show active and running workers output$worker_status <- renderText({ active <- active_worker() running <- names(reactiveValuesToList(worker_states)) sprintf("Active worker: %s\nRunning workers: %s", active, paste(running, collapse=", ")) }) } ui <- fluidPage( selectInput("selected_worker", "Select Worker", choices = c("worker1", "worker2", "worker3")), actionButton("switch_worker", "Switch Worker"), textOutput("worker_status"), # Add worker-specific inputs/outputs here textInput("worker_1_input", "Worker 1 Input"), textInput("worker_2_input", "Worker 2 Input"), textInput("worker_3_input", "Worker 3 Input") ) ``` This setup provides: 1. Multiple concurrent workers, each with their own state and observer 2. Only the active worker processes inputs 3. Automatic cleanup of idle workers after 30 minutes of inactivity 4. Ability to switch between workers 5. State tracking for all running workers You can customize the idle timeout duration and add more sophisticated state management or cleanup logic based on your needs. You might also want to add: - Worker initialization/shutdown hooks - State persistence between sessions - More granular worker status tracking - Error handling for worker operations Would you like me to expand on any of these aspects or show how to implement additional features? > How does the `cleanup_observer` created for each worker via `create_worker` know which observer to destroy with the `cleanup_observer$destroy()` call? Is there some invisible namespacing going on or something? There's no invisible namespacing - it's actually leveraging JavaScript-style closures! When `create_worker` creates both observers, they form a closure over the variables in their creation environment. Here's a simpler example to demonstrate: ``` create_linked_observers <- function() { # This value exists in the function's scope my_observer <- observeEvent(input$something, { # Logic here }) # This second observer can "see" my_observer because of closure cleanup <- observe({ # Can reference my_observer here because it was in scope # when this observer was created my_observer$destroy() }) } ``` When the cleanup observer uses `observer$destroy()` in the original code, it's referencing the specific `observer` variable that was in scope when that cleanup observer was created. Each call to `create_worker` creates a new function scope with its own `observer` variable, so each cleanup observer closes over its own corresponding worker observer. ... Ah, so I guess it's something about the `list()` that gets returned... into the server's scope? We have a little private environment that just sort of... lives there? I *really* don't have a strong grasp on *what exactly happens inside `server.R`* -