hafen.client: Addons and Libraries
hafen.client():addons() is every addon the client discovered, one handle per id. An addon that exports a table is a library: another addon reads it with addon:api() and calls into it through the client.
local library = hafen.client():addons():get("mylib") -- a handle, whether or not mylib is installed
local api = library:api() -- its export, or nil
if api then api.show("Hello") else hafen.log():write("Hello") end
The collection
| Method |
Returns |
Permission |
Description |
hafen.client():addons() |
collection |
Unprotected |
Every addon the last load found in addons/, by id. One object per addon of yours. |
addons:list(filter) |
Addon[] |
Unprotected |
The handles, by id. A string filter is a substring test on the id; a function is a predicate over the handle. |
addons:count(filter) |
number |
Unprotected |
How many. |
addons:find(filter) |
Addon \| nil |
Unprotected |
The first that matches. |
addons:get(id) |
Addon |
Unprotected |
Always a handle, the same object per id. :exists() is the question a nil would answer. |
addons:export(t) |
the collection |
Unprotected |
Publish t as your export, once — below. |
pairs, # and [n] are refused naming :list(), as on every collection. Everything here answers as of the last load: a folder that appears or goes mid-session is seen at the next reload, like the AddOns manager.
The handle
| Method |
Returns |
Permission |
Description |
addon:id() |
string |
Unprotected |
The id, as you addressed it. |
addon:exists() |
boolean |
Unprotected |
A folder with a manifest.json at the last load. |
addon:info() |
Addon \| nil |
Unprotected |
The snapshot: id, name, version, author, description, status, reason. nil for an id that does not exist. |
addon:api() |
table \| nil |
Unprotected |
Your copy of its export — below. nil when it is not loaded, exported nothing, or is below the minimum you declared. |
| Rule |
Detail |
status |
loaded, disabled, not loaded, error, outdated, auto-disabled or manifest error: the words :addons prints. reason is the sentence behind an error, outdated, auto-disabled or manifest error, absent otherwise. |
| Interned |
addons:get("mylib") == addons:get("mylib"), from the first call, whether or not the folder exists. tostring is Addon(mylib). |
| Closed |
An unknown verb raises naming the verbs a handle answers. |
Dependencies and load order
Your manifest’s dependencies and optional_dependencies list ids, each as <id> or <id>>=MAJOR.MINOR.PATCH (manifest). They decide when your files run and what happens when a dependency is missing.
| Rule |
Detail |
| Order |
Every addon runs after the installed, loading addons its two lists name, ties by id. A dependency’s export and Load precede your file body. |
| A hard dependency missing |
Your addon is a load error, its status reading error: needs mylib, which is not installed (is disabled, is out of date, has a manifest error, failed to load). Nothing of yours runs. |
| A minimum |
mylib>=1.2.0 compares the library’s manifest version as the hub orders versions. Not met on a hard dependency: error: needs mylib >= 1.2.0, 1.0.0 installed. On an optional one: the library is, to you, absent — :api() is nil, :exists() and :info() still answer. A library whose version is not a version is below every minimum. |
| An optional dependency absent |
Nothing happens. Ask :api() when you need it. |
| A bundle |
A manifest with "bundle": true requires none of its dependencies: they order its load like any, and one that is off, missing or below its minimum is, to it, what an absent optional dependency is. Its :api() is nil, and one stopped mid-session leaves the bundle running (bundles). |
| A cycle |
Hard dependencies that close a cycle are a load error on each member: depends in a cycle: a -> b -> a. An optional dependency that would close one is not ordered, with a log line. |
| Mid-session |
A library the CPU budget stops takes its loaded hard dependants with it, each reading auto-disabled (needs mylib). An optional dependant keeps running and its :api() turns nil. |
| Tearing down |
In reverse load order: a dependant’s Disable handler still reaches its library. |
-- optional: ask at the moment of use, and nothing breaks without it
local library = hafen.client():addons():get("mylib")
local function notify(text)
local api = library:api()
if api then api.show(text) else hafen.log():write(text) end
end
Exporting
hafen.client():addons():export(t) reads t once, at the call, and keeps a copy. Call it in your file body, so every addon that names you in a dependency list finds it. A second call refuses already exported.
t may hold |
Rule |
| Functions |
Each runs as yours when called — below. |
| Strings, numbers, booleans |
Copied. A value that changes is a function that answers it: the copy never updates. |
| Tables of the same |
Copied, recursively. |
| Anything else |
Refused naming the key: export: 'icon' is a Widget — export functions and plain values. A handle of yours is not a value another addon can hold. |
local open = 0
local function show(text)
open = open + 1
local box = hafen.ui():widget():size(240, 28):position(400, 40 + open * 32)
hafen.ui():label():text(text):parent(box):position(8, 6)
hafen.timer():after(3, function() box:destroy(); open = open - 1 end)
return open
end
hafen.client():addons():export({ show = show, count = function() return open end })
Reading an export
handle:api() hands you your own copy of the library’s export, the same table on every call while the library is loaded.
| Rule |
Detail |
| Read-only |
A write refuses: mylib's export is read-only — it is your copy; a change belongs in the library, through a function it exports. pairs and # work; it is a table. |
| Functions are wrappers |
api.show == api.show, and rawequal(api.show, f) is false for the library’s own f. Every call enters the library. |
nil |
The library does not exist, is not loaded, exported nothing, or is below the minimum your manifest names. |
| After a teardown |
The copy you hold stays a table; a function in it refuses mylib.show: mylib is disabled. |
What a call does
A call through an export — and a callback the library calls back — enters the owner’s door.
| Rule |
Detail |
| Runs as its owner |
The library’s code runs under the library’s consent, with the library’s environment and store, whoever called it. A callback you hand a library runs under yours. |
| Budgets |
The call is an entry into the owner’s Lua: a fresh instruction budget. Its time is charged to the owner under exports and to the caller’s own entry. |
| An error |
Reaches the caller as mylib.show: <reason>; the library keeps running. pcall catches it. |
| Busy |
From a handler that holds a tree’s monitor (threading), a library busy on another thread refuses: mylib.show: mylib is busy on another thread. |
What crosses, and how
Arguments and return values follow one rule in both directions.
| Value |
Crosses as |
A string, number, boolean, nil |
Itself. |
| A function |
A wrapper entering its owner’s door. The same function crosses as the same wrapper. |
| A table |
A read-only copy, recursive, cycles kept; the write refusal names the addon it came from. A metatable does not cross. |
| A bridge handle (a widget, a timer, a session, a store table, a collection…) |
Refused: mylib.show: argument 2 is a Widget — a handle does not cross to another addon; hand it a function. |
-- lend a capability, not a handle: the library can only do what your functions do
local settings = hafen.store():var("settings")
api.bind({ get = function(key) return settings[key] end,
set = function(key, value) settings[key] = value end })
See Also