Data, Files, and Network #
Budo gives apps practical I/O without asking them to become platform specialists. You can persist data with SQLite, read bundled files, fetch HTTP resources, and exchange UDP datagrams. The APIs are small, but they have real policy boundaries so projects remain portable and packageable.
SQLite persistence #
sys.db opens SQLite databases stored as .db files in the project directory. Database names are sanitized and may contain only letters, numbers, underscores, and hyphens.
// Open or create a SQLite database named notes.db.
const db = sys.db.open('notes');
// Create the table once if it does not already exist.
sys.db.execute(db, `
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0
)
`);Use run for parameterized statements that change data.
// Insert a new note using a parameter instead of string concatenation.
sys.db.run(db, 'INSERT INTO notes (title) VALUES (?)', 'Sketch the menu');
// Update the note with id 1 and store true as SQLite integer 1.
sys.db.run(db, 'UPDATE notes SET done = ? WHERE id = ?', true, 1);Use query for results. Rows come back as JavaScript objects.
// Query rows as JavaScript objects.
const rows = sys.db.query(db, 'SELECT id, title, done FROM notes ORDER BY id DESC');
// Iterate through every returned row.
for (const row of rows) {
// Log a compact representation for development.
console.log(row.id + ': ' + row.title);
}Close database handles when a long-lived tool no longer needs them.
// Close the database handle when the app no longer needs it.
sys.db.close(db);SQLite errors throw in JavaScript and Lua when the call violates the contract or the statement fails. sys.db.getError() returns the last wrapper error string.
File access #
sys.file provides read-only access inside a configured root. On desktop, the root defaults to the project directory and can be changed with --file-root. On Android, bundled assets are readable by default. External shared storage requires an explicit filesystem policy in app.json.
{
"filesystem": true
}All file paths passed to sys.file are relative to the file root. Absolute paths and .. traversal are rejected.
// Read a bundled text file from the configured file root.
const text = sys.file.readText('levels/intro.json');
// Read a bundled binary file as an ArrayBuffer.
const bytes = sys.file.readBinary('images/title.png');
// Check for an optional file before reading its metadata.
if (sys.file.exists('settings.json')) {
// Print the file size in bytes.
console.log(sys.file.size('settings.json') + ' bytes');
}Listings return entries with a name, type, and size for files.
// List entries in a project-relative folder.
for (const entry of sys.file.list('levels')) {
// Each entry reports its type and name.
console.log(entry.type + ': ' + entry.name);
}Hidden files are excluded from listings. exists() checks regular files; use isDirectory() when you care about folders.
HTTP fetch #
The global fetch function and sys.network.fetch use the same network policy. By default, HTTP access is disabled. Declare allowed domains in app.json.
{
"network": ["api.example.com", "cdn.example.com"]
}Use "*" for unrestricted network access during a prototype, then narrow it before release.
// Load scores asynchronously from an allowed domain.
async function loadScores() {
// Make the HTTP request using the runtime fetch implementation.
const response = await fetch('https://api.example.com/scores');
// Treat non-2xx responses as app-level failures.
if (!response.ok) {
// Include the status code so logs are useful.
throw new Error('HTTP ' + response.status);
}
// Parse and return the JSON response body.
return response.json();
}Request options support method, headers, and a string or ArrayBuffer body.
// Send a JSON event to an allowed domain.
const response = await fetch('https://api.example.com/events', {
// Choose the HTTP method.
method: 'POST',
// Tell the server that the request body is JSON.
headers: { 'Content-Type': 'application/json' },
// Serialize the event object into the request body.
body: JSON.stringify({ name: 'start' })
});The JavaScript response object follows the familiar browser shape: ok, status, statusText, url, redirected, type, bodyUsed, headers, and body readers for text, json, and arrayBuffer.
Lua uses a callback form because Lua does not have JavaScript promises in the runtime.
-- Fetch data from an allowed domain.
fetch('https://api.example.com/scores', function(response, error)
-- Handle transport or policy errors first.
if error then
-- Log the error and stop this callback.
console.log('Network error: ' .. error)
return
end
-- Parse the JSON response body into a Lua table.
local data = json_parse(response.body)
-- Log the HTTP status for debugging.
console.log('Status: ' .. response.status)
end)Headers #
JavaScript responses expose a small Headers object.
// Read a response header case-insensitively.
const type = response.headers.get('Content-Type');
// Check for a cache validator before logging it.
if (response.headers.has('ETag')) {
// Print the ETag value.
console.log('Cached resource: ' + response.headers.get('ETag'));
}
// Iterate through all returned headers.
response.headers.forEach((value, name) => {
// Log each header as name and value.
console.log(name + ': ' + value);
});Header names are matched case-insensitively. Body readers can only be called once, mirroring browser Fetch behavior.
UDP sockets #
sys.udp exposes non-blocking datagram sockets on desktop and Android. The current web target does not expose raw UDP because browsers do not provide it.
// Bind to port 0 so the operating system chooses an available port.
const socket = sys.udp.bind(0);
// Read the selected local port.
const port = sys.udp.getPort(socket);
// Log the port so another process can connect.
console.log('Listening on ' + port);
// Register the callback that receives incoming datagrams.
sys.udp.onMessage(socket, (message) => {
// Log the sender address.
console.log('From ' + message.host + ':' + message.port);
// Log the number of received bytes.
console.log('Bytes: ' + message.data.length);
});
// Send two bytes to a local UDP endpoint.
sys.udp.send(socket, '127.0.0.1', 5000, [0x48, 0x69]);Callbacks are polled once per frame. Each socket stores one message callback; setting another replaces the previous one.
Close sockets when you are finished.
// Release the UDP socket when communication is finished.
sys.udp.close(socket);RTP-MIDI sessions are built on the UDP wrapper and are exposed through sys.midi, not sys.udp.
Error style #
Budo bindings follow a consistent rule. Programmer errors throw or raise: wrong argument counts, invalid types, unsafe paths, invalid SQL, and contract violations. Transient runtime failures return a falsy value where that is more useful: missing devices, absent files, unavailable features, exhausted slots, and refused network operations. Namespaces that can fail expose getError() for the last human-readable message.
// Try to read a file that may not exist.
const text = sys.file.readText('missing.txt');
// The file API returns null for missing readable content.
if (text === null) {
// Read the namespace's last error message for a user-facing detail.
console.log(sys.file.getError());
}Use exceptions for mistakes you should fix during development. Use return values and capability checks for conditions a user or platform can reasonably create.