3. Replace src/main.luau
Navigation · State · Data · Host · about 30 minutescomplete routed and persisted application
One router owns navigation, one versioned snapshot carries the router plus filter, and RequestIds correlate asynchronous storage completions.
When to use it
Replace the entire generated entry module. Keep encode and restore near each other so snapshot version, fields, validation, and route restoration remain auditable.
- Navigation.compile validates note_id before a route can enter the stack.
- State.decode checks framing and version; Data.decode then checks the decoded field shapes; the application finally admits only known filter values.
- router.restoreEncoded validates every restored route before replacing the active stack.
- app.resolve ignores unknown RequestIds and clears known requests before applying their result.
- Rendering reads current state; storage effects start only from admitted button actions.
Focused example
--!strict
local Data = require("luastra/data")
local Host = require("luastra/host")
local Navigation = require("luastra/navigation")
local State = require("luastra/state")
local UI = require("luastra/ui")
local routes = Navigation.compile {
{ name = "library", path = "/" },
{
name = "note",
path = "/notes/:note_id",
params = { note_id = { type = "integer", minimum = 1, maximum = 99 } },
},
}
local router = Navigation.createRouter {
compiler = routes,
initial = { name = "library", params = {}, query = {} },
}
local snapshotSchema = Data.object({
navigation = Data.string({ minBytes = 5, maxBytes = 4096 }),
filter = Data.string({ minBytes = 3, maxBytes = 9 }),
})
local App = require("luastra/app")
local Root = {}
local filter = "all"
local message = "Nothing saved yet"
local pending: { [number]: string } = {}
local function encodeSnapshot(): string
return State.encode(1, {
navigation = router.encode(),
filter = filter,
})
end
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 checked = Data.decode(snapshotSchema, decoded.fields)
if not checked.success then return false end
local fields = checked.value :: { [string]: any }
if fields.filter ~= "all" and fields.filter ~= "favorites" then return false end
local restored = router.restoreEncoded(fields.navigation)
if not restored.success then return false end
filter = fields.filter
return true
end
function Root.render(): UI.Node
local current = router.current()
local location = router.currentLocation()
return UI.Screen {
id = "reading-list",
documentTitle = "Routed reading list",
UI.Column {
id = "reading/content",
width = "content",
padding = "responsive",
gap = "md",
UI.Text { id = "reading/title", text = "Routed reading list", variant = "title" },
UI.Text { id = "reading/route", text = "Route: " .. current.name .. " (" .. location .. ")" },
UI.Text { id = "reading/filter", text = "Filter: " .. filter },
UI.Text { id = "reading/message", text = message, role = "status" },
UI.Actions {
id = "reading/navigation",
UI.Button { id = "reading/open", text = "Open note 7", onTap = "reading.open" },
UI.Button {
id = "reading/back",
text = "Back to library",
onTap = "reading.back",
disabled = not router.canBack(),
},
UI.Button { id = "reading/filter-toggle", text = "Toggle filter", onTap = "reading.filter" },
},
UI.Actions {
id = "reading/storage",
UI.Button { id = "reading/save", text = "Save view", onTap = "reading.save" },
UI.Button { id = "reading/load", text = "Restore view", onTap = "reading.load" },
},
},
}
end
function Root.handle(action: string, target: string, _value: string)
if action == "reading.open" and target == "reading/open" then
local result = router.push { name = "note", params = { note_id = 7 }, query = {} }
message = result.success and "Opened note 7" or "Could not open note"
elseif action == "reading.back" and target == "reading/back" then
if router.back() then message = "Returned to library" end
elseif action == "reading.filter" and target == "reading/filter-toggle" then
filter = if filter == "all" then "favorites" else "all"
message = "Filter changed"
elseif action == "reading.save" and target == "reading/save" then
track(Host.storageSet("reading-list-view", encodeSnapshot()), "save")
message = "Saving…"
elseif action == "reading.load" and target == "reading/load" then
track(Host.storageGet("reading-list-view"), "load")
message = "Loading…"
elseif action == "reading.restore" and target == "app" then
message = if Root.restore(_value) then "View restored" else "Saved view 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 = "View saved"
elseif Root.restore(payload) then message = "View restored"
else message = "Saved view is invalid" end
end
function Root.snapshot()
return {
name = router.current().name,
location = router.currentLocation(),
filter = filter,
message = message,
encoded = encodeSnapshot(),
}
end
return App.compose {
render = function(_outputs) return Root.render() end,
handle = Root.handle,
resolve = Root.resolve,
snapshot = Root.snapshot,
}