object
Class declarations, state, hooks, and calling instances.
Declare a class
local Auction = object "Auction" {
migrations = "migrations/Auction",
publishes = { "bid", events = "self" },
receives = { ["Account:flagged"] = function(state, event) end },
expire = "30d",
admit = function(name) return #name > 3 end,
directory = {
from = function(state) return state.store:get("lot") end,
fields = { status = f.string },
},
hooks = {
init = function(state) end,
alarm = function(state) end,
follow = function(state, topic, follower) return true end,
},
bid = function(state, user, amount) end, -- a method
}
Seven reserved keys; every other key is a method.
| Key | Meaning |
|---|---|
migrations | directory of .sql files that build each instance's schema |
publishes | topics this class emits; name = "self" or = "public" sets a policy instead of using the hook |
receives | ["Class:topic"] = handler for streams it consumes; the class must be declared in the project |
expire | a duration string; an instance untouched that long is deleted. Touch renews it, a pending alarm postpones it |
admit | function(name), decides whether a NEW instance may exist; truthy admits. A refused name leaves nothing behind |
directory | { from, fields }: one queryable row per instance; see Directory |
hooks | init, alarm, follow |
A class name starting with __ is reserved for the platform and
refused. Referencing a class declared elsewhere:
local Auction = objects "Auction"
Instances
Auction("lot-42"):bid("ada", 25) -- callable form
Auction:get("lot-42"):bid("ada", 25) -- same thing
Arguments and return values must be JSON-representable. A name is yours: any non-empty string, and it says nothing about where the object lives, which is the platform's business.
Class verbs
Auction:list { where = { status = "open" }, order = { closes_at = "asc" }, limit = 50 }
Auction:find { status = "open" }
Auction:visit { where = { status = "open" }, limit = 50 }
All three read the class's directory and
wake nothing. A class without a directory declaration has no rows to
read.
state
| Field | Meaning |
|---|---|
state.sql | this instance's private database |
state.store | key-value face on the same storage |
state.name | instance name |
state.now() | platform clock, milliseconds |
state:set_alarm(duration) | schedule the alarm hook |
state:destroy() | ends this instance after the current call replies: storage, snapshot and edges reclaimed, the name recreatable fresh |
state:publish(topic, event) | emit on a declared topic; an undeclared topic raises |
state:follow(handle, topic, filter?) | follow another object; returns nothing, raises follow refused when the publisher declines |
state:unfollow(handle, topic) | drop that edge |
state:followers(topic?) | current followers, every topic when none is named |
state:drop_followers(handle) | drop every edge an identity holds |
state:method(...) | a sibling method of this class, run directly in this call |
| any other key | in-memory scratch, lost when the object sleeps |
state.sql takes the same exec / query / query_one / batch as
a database, minus the read variants.
state.store
The key-value face on the same file, for state too small to deserve a
table. No migrations key, no declaration at all:
local Counter = object "Counter" {
hit = function(state)
local n = (state.store:get("count") or 0) + 1
state.store:set("count", n)
return n
end,
}
The verbs are kv's, page shape included:
| Method | Returns |
|---|---|
get(key) | the value, or nil |
set(key, value) | nothing; a nil value is a delete |
set_batch(table) | nothing; writes every pair, deletes the nil ones |
delete(key) | nothing; one key, where kv takes several |
list(options?) | one page: { entries, cursor } |
- One transaction. A call that touches
state.storeandstate.sqlcommits once; the two faces are one file and can never disagree. - Values are JSON-representable and at most 65,536 bytes each as
stored text. Past that, the write is refused naming the key, and the
state wants a table in
state.sql. listtakes{ prefix?, limit?, cursor? }; entries are{ key, value }in ascending key order,limitdefaults to 100 and a value outside 1 to 1000 is refused, and a page without acursoris the last.- The pairs live in a reserved table that SQL cannot see, from
methods and the console alike. Listing them is what
listis for.
Hooks
hooks = {
init = function(state) end,
alarm = function(state) end,
follow = function(state, topic, follower) return true end,
}
| Hook | Runs |
|---|---|
init | once per instance ever, before its first call proceeds |
alarm | when a set alarm comes due |
follow | to decide whether follower may follow topic; truthy accepts |
Hooks are called by the platform. Calling one through a handle is
refused: 'init' is a platform hook; only the platform calls it.
With migrations declared, the files apply before init, so init
only ever seeds rows. Without it, init is where the tables get
created.
follower
The identity a follow hook receives:
| Field | Meaning |
|---|---|
follower.name | instance name it speaks as |
follower.id | "Class/name" |
follower.transport | "object" or "connection" |
follower:is(Class) | class check against a class value |
Rows from state:followers carry the same fields plus topic.
Durations
set_alarm and workflow timeouts take "500ms", "30s", "10m",
"24h", "7d", or a bare number of seconds. expire takes the string
form only, and wants at least a second.
Gotchas
- A method cannot be named
init,alarm,follow,receive,receives,hooks,admitordirectory, and a name starting with_is not callable through a handle either. hooks.receiveis not a hook. Stream handlers live underreceives, keyed by"Class:topic"; the old spelling is refused with a message saying so.- Scratch keys on
statelook durable and are not. Anything needed after the object sleeps goes instate.storeorstate.sql. state.sqlhas noreadvariants. An object's storage has no replica to read from.