// functions/rooms.js
export class Room {
constructor(state, env) { this.state = state; this.members = 0; }
async join(name) {
const seen = (await this.state.storage.get("seen")) ?? [];
await this.state.storage.put("seen", [...seen, name]);
return ++this.members;
}
async fetch(request) { return new Response("room " + request.method); }
async alarm() { /* a stored alarm is due */ }
}
export default {
async fetch(request) {
const rooms = substrio.objects.Room;
const room = rooms.get(rooms.idFromName("lobby"));
return Response.json({ members: await room.join("kris") });
},
};
Classes and ids
Each named class export of a function defines an object class with that name.
A function's prototype is assignable, while a class's is not. The default
export does not define an object class. Names are read from the evaluated module.
If two functions in an app export the same class name, the first to load registers
it and the second fails to load. The error names the class and both functions.
Deployment cannot detect this because it does not run the module. A module may
have no default export.
substrio.objects.Name has idFromName(text) (the same text always names the same
object; the id is the first 16 bytes of sha256 of the class and the text),
newUniqueId(), idFromString(hex), and get(id), which returns a stub.
stub.fetch(request) calls instance.fetch(request); any other member call,
stub.join("a"), calls that method with the arguments and returns its result.
Arguments and results preserve Date, BigInt, Map, Set, ArrayBuffer, and typed arrays;
a value with a cycle or a function throws a TypeError. A method that throws
fails the call with its message. Request and response bodies are raw bytes.
An app cannot access another app's objects. The calling isolate supplies the app as part of the object's identity; code cannot set it.
One at a time
Each object has a mailbox. A call waits until the turn before it has settled (its promise resolved and its storage committed). Awaits inside a turn do not let another call in. Exactly one instance of an object exists at a time, and a turn that awaits (fetch, another object) does not hold up other objects.
A call that finds 10,000 calls already waiting on the mailbox is refused with
429. An object that calls itself through state.objects is refused at once; a
call through the global substrio.objects is not recognised as a self-call, so
a self-call or a cycle that way waits until the call's wall limit ends that
turn.
state.blockConcurrencyWhile(fn) runs fn and holds every other call on the
mailbox until it settles, for setup that must finish before any other turn
reads the object's fields. state.waitUntil(promise) is awaited before the
turn's reply is released, the same as ctx.waitUntil in concurrency.md.
Storage
state.storage provides get(key | keys), put(key, value | entries),
delete(key | keys), deleteAll(), list({ start, end, prefix, limit, reverse }),
transaction(fn), getAlarm(), setAlarm(time), and deleteAlarm(). list
returns a Map in key order. An error or txn.rollback() discards a transaction's writes.
A turn reads its writes. Its writes and alarm are applied together when the turn ends, before the reply is released. The commit includes all changes together.
Alarms
setAlarm(ms since the epoch | Date) stores one alarm per object. When it is due,
the object receives a turn calling alarm({ retryCount, isRetry }). The alarm
survives a restart and fires immediately if overdue. A failed alarm() is retried
6 times with doubling delays starting at 1,000 ms, then dropped.
WebSockets
A client upgrades GET /v1/_objects/ws?class=Room&name=lobby using the app's
credentials. Use &id=<32 hex digits> instead of name to select by id. The
object's fetch(request) receives the upgrade request with request.webSocket
and accepts it with state.acceptWebSocket(request.webSocket). An unaccepted
socket or a throwing fetch closes with 1008 or 1011.
Each client frame becomes a turn calling webSocketMessage(ws, message), with a
string or an ArrayBuffer for a binary frame. Frames run in arrival order and one
at a time with other object calls. A client close calls
webSocketClose(ws, code, reason, wasClean). Use ws.send(data) and
ws.close(code, reason) to respond, and state.getWebSockets() to list accepted
sockets. A socket whose unsent backlog exceeds 64 MB closes with code 4290 after
previously queued frames. A client refused by the mailbox closes with 1013.
Eviction does not close sockets; the next frame creates the instance again over
stored state. A server restart closes all sockets, and the client must reconnect.
Eviction
An object with no turn for 30 s loses its in-memory instance (its fields); the next call runs the constructor again over the stored state. A redeploy or a recycled isolate does the same.