Skip to content

Background threads

On Android, the Godot part of Nari stops when the app goes to the background, but the audio (written in Rust) keeps playing. Threads run next to the audio, so they keep working with the screen off: scrobbling, webhooks, streaming and sign-in servers.

main.lua
local worker = nari.thread("worker.lua")
worker:on_message(function(message) nari.log("worker says " .. message.text) end)
worker:send({ text = "hi" })
worker.lua
nari.on("message", function(message)
nari.send({ text = message.text .. " back" })
end)

The handle has send(value), on_message(handler), on_error(handler) (called with the error if the thread crashes) and stop(). Threads also stop when the plugin is turned off.

Threads are event driven: the script runs once to set up its handlers, then the thread sleeps until an event, a message or a timer arrives. A slow nari.http only blocks its own thread.

Function or event
nari.on("message", handler) A value sent with handle:send.
nari.on("track", handler) The current song changed (nil when the queue ends). A thread that starts mid-song gets it right away.
nari.on("state", handler) { playing, position } when playback starts or pauses.
nari.on("ask", handler) A question from another thread: handler(value, reply).
nari.send(value) Sends a value to main.lua, delivered when the main thread is running again.
nari.after(seconds, handler) A timer.
nari.http(request) Blocking: returns response, problem directly. Add stream = function(chunk) end to get the body piece by piece (return false to stop); streamed requests have no time limit.
nari.tcp(spec) Blocking connection to a TCP server. See Sockets.
nari.serve(handler, options) A local web server, answered even while Android keeps the app in the background.
nari.ask(name, value, timeout) Blocking question to another thread. See Threads asking threads.
nari.resolver(scheme, handler), nari.source(scheme, handler) Stream songs yourself. See Streaming.
nari.analyze(path, options) Blocking audio analysis.
nari.get, nari.set, nari.log, nari.warn, nari.plugin, JSON, Base64, hashes, nari.cipher, nari.modpow, nari.random_bytes Same as on the main thread.
require Loads Lua modules from the plugin folder.
local socket, problem = nari.tcp({ host = "example.com", port = 4070 })
socket:send(hello)
local header = socket:receive(4, 10) -- exactly 4 bytes, waiting up to 10 seconds
socket:on_data(function(bytes) end) -- from now on, bytes arrive as events
socket:on_close(function(problem) end)
Key
host, port Required.
tls true for an encrypted connection, checked against the usual certificate authorities.
timeout Seconds to wait for the connection, 20 by default.
  • nari.tcp returns nil, problem when it can’t connect, and socket:send(bytes) returns true or nil, problem.
  • socket:receive(count, timeout) blocks until count bytes arrived (any amount when left out). It returns nil, "timeout" or nil, "closed" when it can’t; bytes that arrived before a timeout wait for the next call.
  • socket:on_data(handler) hands every piece to handler between the thread’s other events, so the thread keeps answering messages while the connection is open. After it, receive can’t be used.
  • socket:close() closes it. Sockets also close when the thread stops. Only threads have sockets.

A thread started with a name answers questions from the plugin’s other threads. One thread can own a connection or a login and serve everyone else, even while Android keeps main.lua paused:

main.lua
nari.thread("session.lua", { name = "session" })
nari.thread("audio.lua")
session.lua
nari.on("ask", function(question, reply)
if question.cached then
return cache[question.cached] -- answer right away
end
request_key(question.file, function(key) -- or later, from a socket or a timer
reply({ key = key })
end)
end)
audio.lua
local answer, problem = nari.ask("session", { file = id }, 10)

nari.ask(name, value, timeout) waits for the answer (20 seconds by default) and returns it, or nil, problem when the thread doesn’t exist, fails, doesn’t answer in time ("timeout") or calls reply(nil, problem). Only the first answer counts, and a thread can’t ask itself.