← Recipe: persist and restore state

3. Replace src/main.luau

luastra/state · luastra/host · app.resolve · about 15 minutes
complete asynchronous storage lifecycle

Pending request IDs distinguish save and load completions that may arrive out of order.

When to use it

Replace the entire entry module. Keep restore separate so malformed persisted data can be tested without a real host request.

Focused example

--!strict

local Host = require("luastra/host")
local State = require("luastra/state")
local UI = require("luastra/ui")

local App = require("luastra/app")
local Root = {}
local count = 0
local message = "Nothing saved yet"
local pending: { [number]: string } = {}

local function track(id: number, operation: string)
    pending[id] = operation
end

function Root.restore(payload: string): boolean
    local decoded = State.decode(payload, 1)
    if not decoded.success then return false end
    local restored = tonumber(decoded.fields.count)
    if restored == nil or restored < 0 or restored % 1 ~= 0 then return false end
    count = restored
    return true
end

function Root.render(): UI.Node
    return UI.Screen {
        id = "storage-recipe",
        UI.Text { id = "counter/title", text = "Persistent counter", variant = "title" },
        UI.Text { id = "counter/value", text = tostring(count), role = "status" },
        UI.Text { id = "counter/message", text = message, role = "status" },
        UI.Actions {
            id = "counter/actions",
            UI.Button { id = "counter/add", text = "Add", onTap = "counter.add" },
            UI.Button { id = "counter/save", text = "Save", onTap = "counter.save" },
            UI.Button { id = "counter/load", text = "Load", onTap = "counter.load" },
        },
    }
end

function Root.handle(action: string, target: string, _value: string)
    if action == "counter.add" and target == "counter/add" then
        count += 1
    elseif action == "counter.save" and target == "counter/save" then
        local snapshot = State.encode(1, { count = tostring(count) })
        track(Host.storageSet("counter-state", snapshot), "save")
        message = "Saving…"
    elseif action == "counter.load" and target == "counter/load" then
        track(Host.storageGet("counter-state"), "load")
        message = "Loading…"
    elseif action == "counter.restore" and target == "app" then
        message = if Root.restore(_value) then "Loaded" else "Stored state is invalid"
    end
end

function Root.resolve(
    id: number,
    success: boolean,
    payload: string,
    code: string,
    _errorMessage: string
)
    local operation = pending[id]
    pending[id] = nil
    if operation == nil then return end
    if not success then message = operation .. " failed: " .. code return end
    if operation == "save" then message = "Saved"
    elseif Root.restore(payload) then message = "Loaded"
    else message = "Stored state is invalid" end
end

function Root.snapshot()
    return { count = count, message = message }
end

return App.compose {
    render = function(_outputs) return Root.render() end,
    handle = Root.handle,
    resolve = Root.resolve,
    snapshot = Root.snapshot,
}

Complete inventory

View all pages in Recipe: persist and restore state
← Previous: 2. Replace luastra.jsonNext: 4. Replace tests/smoke.luau →