rustploy-gui 0.1.36

Rustploy — desktop client (glacier-ui) for the Rustploy PaaS daemon
--!strict

-- busy.luau — trava de "ação em andamento" para os handlers de
-- on_click/on_submit.
--
-- `install(names)` troca cada função global listada por uma versão que,
-- enquanto roda (inclusive suspensa num `fetch`), mantém `ctx.busy_<nome> =
-- "true"`:
--   • um novo disparo da MESMA ação nesse intervalo é ignorado (duplo clique);
--   • o template liga `disabled = @busy_<nome>` no botão, então ele fica
--     bloqueado na hora e volta assim que a ação termina — com sucesso, erro
--     de resposta ou timeout (o `fetch` tem teto em net/api.luau), e mesmo se
--     o handler lançar erro (o flag é limpo antes de relançar);
--   • o botão libera no PRIMEIRO `toast` da própria ação — é o aviso de que a
--     resposta chegou. Depois dele o handler ainda costuma reler serviço/
--     histórico/snapshot (mais rpcs); esperar isso deixaria o botão travado
--     segundos além da resposta. A ação é identificada pela corrotina
--     (`coroutine.running()`), então toasts de outras ações não interferem.
-- `install` também embrulha o `init()` para semear os flags (ver `seed`).
-- A lista vem de busy_actions.luau; só ações de botão/formulário entram (um
-- callback de sse ou uma `rule` bloqueada por reentrada perderia eventos).

local M = {}

function M.key(name: string): string
    return "busy_" .. name
end

-- Semeia `busy_<nome> = "false"` para toda ação instalada. O template liga
-- `disabled = @busy_<nome>`, e uma chave ausente não resolve. Roda dentro do
-- `init()` (ver `install`): o logout (`disconnect`) apaga o `ctx` inteiro e
-- chama `init()` de novo, então os flags renascem junto com os demais
-- defaults.
local function seed(names: { string }): ()
    for _, name in names do
        ctx[M.key(name)] = "false"
    end
end

-- Quem está rodando: `running[nome] = corrotina dona`; `held[corrotina]` =
-- chaves que ela segura (uma ação pode chamar outra, também embrulhada).
-- Código Lua que chama um handler direto (sem corrotina própria) roda na
-- principal, onde `coroutine.running()` é nil: `MAIN` é o dono dessas
-- chamadas.
local MAIN = {}
local running: { [string]: any } = {}
local held: { [any]: { string } } = {}

local function owner(): any
    return coroutine.running() or MAIN
end

local function release(thread: any): ()
    local keys = held[thread]

    if keys then
        for _, key in keys do
            if running[key] == thread then
                running[key] = nil
                ctx[key] = "false"
            end
        end
    end
end

function M.install(names: { string }): ()
    local env = _G :: any
    local installed: { string } = {}

    for _, name in names do
        local handler_function = env[name]

        if type(handler_function) == "function" then
            local key = M.key(name)
            table.insert(installed, name)

            env[name] = function(...)
                if running[key] then
                    return
                end

                local thread = owner()
                running[key] = thread
                ctx[key] = "true"
                held[thread] = held[thread] or {}
                table.insert(held[thread], key)

                local call_results = table.pack(pcall(handler_function, ...))

                -- Larga só a chave DESTA ação (a externa, se houver, segue
                -- presa).
                if running[key] == thread then
                    running[key] = nil
                    ctx[key] = "false"
                end

                local keys = held[thread]
                if keys then
                    table.remove(keys, table.find(keys, key))
                    if #keys == 0 then
                        held[thread] = nil
                    end
                end

                if not call_results[1] then
                    error(call_results[2], 0)
                end

                return table.unpack(call_results, 2, call_results.n)
            end
        end
    end

    local real_toast = env.toast
    env.toast = function(toast_options)
        release(owner())

        return real_toast(toast_options)
    end

    local init = env.init
    env.init = function(...)
        seed(installed)

        if type(init) == "function" then
            return init(...)
        end
    end
end

return M