# shared_state.R
#
# Shared state between Shiny modules without passing a chain of arguments through every level:
# the pattern from the AthlyticZ session "The No-Look Pass" (Veerle Eeftink-van Leemput), as one
# small file you can source() into any app.
#
#   source("shared_state.R")
#
#   server <- function(input, output, session) {
#     store <- shared_state(session, team = NULL, season = 2026)   # create once, at the top
#     pickerServer("pick")                                         # writes store$team
#     headToHeadServer("h2h")                                      # reads  store$team
#   }
#
#   pickerServer <- function(id) moduleServer(id, function(input, output, session) {
#     store <- shared_state(session)                                # fetch it anywhere, no arguments
#     observeEvent(input$team, store$set("team", input$team))
#   })
#
#   headToHeadServer <- function(id) moduleServer(id, function(input, output, session) {
#     store <- shared_state(session)
#     output$title <- renderText(paste("Head to head:", store$get("team")))   # reactive read
#   })
#
# How it works, in two sentences. The session object is one shared object for the whole app, and
# session$userData is a plain environment on it that every module can reach without being handed
# anything; the store keeps a reactiveValues() there, so a read inside a reactive context
# re-runs when the value changes, and the top-level and module sessions resolve to the same
# root session so they see the same store. Nothing is global, two apps in one process never
# share state, and testServer() works because each test gets its own session.
#
# What it does not do. It is not namespaced: a key is one key for the whole app, so name keys
# for what they are ("team", "season"), not for the module that writes them. If two modules
# write the same key you have a design problem, not a bug in this file.
#
# License: MIT. Written for athlyticz.com/skills from the method the session teaches; the
# session's own app and code come with the recording.

library(shiny)

shared_state <- function(session = shiny::getDefaultReactiveDomain(), ...) {
  stopifnot(!is.null(session))
  root <- session
  while (!is.null(root$rootScope) && !identical(root$rootScope(), root)) root <- root$rootScope()
  if (is.null(root$userData$.shared_state)) {
    init <- list(...)
    root$userData$.shared_state <- do.call(shiny::reactiveValues, init)
  } else if (length(list(...))) {
    warning("shared_state() already exists on this session; initial values ignored", call. = FALSE)
  }
  rv <- root$userData$.shared_state
  structure(list(
    get  = function(key) rv[[key]],                     # reactive read: call inside a reactive context
    set  = function(key, value) { rv[[key]] <- value; invisible(value) },
    peek = function(key) shiny::isolate(rv[[key]]),     # non-reactive read, for event handlers
    keys = function() shiny::isolate(names(shiny::reactiveValuesToList(rv))),
    rv   = rv                                           # the reactiveValues itself, if you want it
  ), class = "shared_state")
}

print.shared_state <- function(x, ...) {
  cat("<shared_state>", paste(x$keys(), collapse = ", "), "\n")
  invisible(x)
}

# A minimal runnable example. Run with: shiny::runApp(shared_state_demo())
shared_state_demo <- function() {
  pickerUI <- function(id) selectInput(NS(id, "team"), "Team", c("Phoenix", "Seattle", "Las Vegas"))
  pickerServer <- function(id) moduleServer(id, function(input, output, session) {
    store <- shared_state(session)
    observeEvent(input$team, store$set("team", input$team), ignoreInit = FALSE)
  })
  readerUI <- function(id) h3(textOutput(NS(id, "title")))
  readerServer <- function(id) moduleServer(id, function(input, output, session) {
    store <- shared_state(session)
    output$title <- renderText(paste("Selected without any arguments:", store$get("team")))
  })
  shinyApp(
    ui = fluidPage(pickerUI("pick"), readerUI("read")),
    server = function(input, output, session) {
      shared_state(session, team = "Phoenix")
      pickerServer("pick")
      readerServer("read")
    }
  )
}
