Kinds
rules.js exports each kind by name, such as game, as an object holding the handlers that run every room of that kind. Your client makes a room of it with tinylib.rooms.create('game').Every named export that's an object with a create function is a kind. Tinylib ignores other exports, so you can
export helpers too. publish refuses rules with no kind, or with a kind that breaks a rule on this page.
Tinylib runs a room's inputs one at a time, in the order they arrive, each on the state the last one left. Each input runs one handler:
create and actions answer a client's call: ctx.refuse rejects the call with refused, and a throw rejects it
with failed. The other inputs have already happened, so they can't refuse. If one of them throws, its changes are
dropped but the input stands: someone who joined is still a member. Every error shows on
your developer page.
After each input that neither refuses nor throws, Tinylib calls view, status, and canInvite for every member
with the new state. They get no ctx, and Tinylib may call them again at any time, so compute their result from
their arguments alone; changes they make to state aren't kept. If one throws for a member, that member keeps what
they had, and the input still stands.
No handler runs when a room ends, whether by ctx.end, by its last member leaving, or by going unused for its
idleMs. Record results as they happen.
The rules run under these conditions:
- Handlers are synchronous.
devandpublishrefuse anasynchandler, and a handler that returns a promise fails. - The rules can't reach the network:
fetch,XMLHttpRequest, andWebSocketaren't there. rules.jscan import your own files and npm packages. Fromtinylib-sdkit can import types andtinylib-sdk/kits:defineGame, the seats, the words,addDays,seeded,shuffled,clock, andtodaygiven a date run in rules, and the rest call the page'stinylib. Any other import oftinylib-sdkstopsnpx tinylib-sdk devandpublishwith an error.- Keep everything a room needs in
state. A module-level variable may last between inputs and be shared between rooms, or it may be lost at any time. - Use
ctx.nowinstead ofDate.now(), andctx.random()instead ofMath.random(). stateis saved as JSON after each input, so aMapor anundefineddoesn't survive. An input that would leave the state and its timers past 1 MB changes nothing:createand actions reject withlimit, and any other handler's changes are dropped and show on your developer page.- A handler that runs past 50 milliseconds of CPU time is stopped, and its input fails as if it had thrown.
console.logshows in the browser's console innpx tinylib-sdk dev. In a published app it goes nowhere.
To type a kind, write import type { Kind } from 'tinylib-sdk/rules' in rules.ts, or annotate it with
@type {import('tinylib-sdk/rules').Kind} in rules.js.
Type parameters
- <State>optional
- The type of the room's state.
Default:
any- <Options>optional
- The type of the options
createreceives. Default:
any- <View>optional
- The type
viewreturns. Default:
unknown
Properties
- addressstringoptional
- The path your app opens at when someone comes to a room of this kind from outside the app, like a notification or an invitation. Write it without a leading
/.:idbecomes the room's id, as in'game/:id'. Default: the start screen
- idleMsnumberoptional
How long a room of this kind lasts unused, in milliseconds, counted from the last time a member had it open, acted, joined, or left. Timers don't count. It must be from 1 minute to 365 days.
Members keep a room that ends this way in their past rooms, with its last status and its room data.
Default: 7 days
- JavaScriptidleMs: 365 * 24 * 60 * 60 * 1000 // a list kept for a year without anyone opening itJavaScriptidleMs: 60 * 60 * 1000 // a game played in one sitting, closed after an hour unused
- offlinetrueoptional
- Set to
trueto let members act in a room of this kind without a connection, as in a shared list. Every member gets the whole state, so the kind has noviewand keeps no secrets. The state must fit in 256 KB. - JavaScriptexport const list = { offline: true, create() { return { items: [] } }, actions: { add(state, item, ctx) { if (typeof item?.id !== 'string' || typeof item.text !== 'string') ctx.refuse("That item couldn't be added.") if (!state.items.some((each) => each.id === item.id)) state.items.push({ id: item.id, text: item.text, done: false }) }, }, }
- guestsbooleanoptional
- Set to
falseto keep people without a Tinylib account out of this kind: they can't create a room of it, or join one by its link or code. By default anyone can. See guests. Default: true
- beatMsnumberoptional
- How often
beatruns, in milliseconds, for a game that moves in real time. It must be from 50 milliseconds to 1 second, and a kind withbeatneeds it. For anything slower, set a timer. - JavaScriptbeatMs: 100 // ten times a second
Functions
- create(options, ctx)
- Makes a new room's first state.
- actionsRecord<string, Handler<State>>optional
- The kind's actions, by name.
room.act('move', 4)runsactions.move(state, 4, ctx). A call naming an action the kind doesn't have fails. - timersRecord<string, Handler<State>>optional
- The kind's timers, by name.
ctx.setTimer('turn', at, data)runstimers.turn(state, data, ctx)atat, withctx.usernull. - JavaScripttimers: { turn(state) { state.turn = state.turn === 'x' ? 'o' : 'x' }, },
- beat(state, relays, ctx)optional
- Runs every
beatMswhile at least one member has the room open, with every relay members sent since the last beat, in the order the room took them. - join(state, via, ctx)optional
- Runs after someone joins, once they're a member.
- leave(state, why, ctx)optional
- Runs after a member leaves or is removed.
- presence(state, present, ctx)optional
- Runs when a member arrives or goes.
- migrate(state, fromVersion, ctx)optional
- Reshapes a room's state for a new version, so your other handlers see only the new shape.
- view(state, member, members)optional
- Returns what
membersees of the state, which their client gets throughroom.onView. - status(state, member, members)optional
- Returns the line Tinylib shows for this room in
member's list of rooms, or null for none. - canInvite(state, member, members)optional
- Returns whether
membermay invite now, with an invitation or the room's link.
join doesn't run for the creator. A refusal, a throw, or a first state past the room's limits rejects the client's call, and no room is made.Parameters
- optionsOptions
- What the client passed as
optionstotinylib.rooms.create, or{}. Any user can send anything, so check it before you use it.
Returns
- State
- The room's first state. Returning
undefinedfails the call.
beatMs while at least one member has the room open, with every relay members sent since the last beat, in the order the room took them. The first beat comes beatMs after a member opens the room, and the beats stop once nobody has it open. Positions travel once, as relays: other members draw them at once, and the rules check and keep them here. Send what happens once, such as a hit or a pickup, with room.act.canInvite. A full room turns joins away before join runs; the most members a room holds is 64.leave.Parameters
- stateState
- The room's state, to change in place.
- whyLeaveReason
- Why they left.
fromVersion can be several versions old.Before a version goes live, the room check runs migrate on
a copy of every room that hasn't ended.
member sees of the state, which their client gets through room.onView. Every kind but an offline one needs it.What view leaves out never reaches that member's device, so view is how a game keeps secrets. Tinylib sends a
member their view when it changes. A view must be JSON, and one past 64 KB isn't sent.
member's list of rooms, or null for none. A status never sends a notification; use ctx.notify for that. A status whose text is past 200 characters isn't set.member may invite now, with an invitation or the room's link. Without canInvite, every member may. Returning anything but true or false counts as a throw.An invitation works while the member who sent it may invite, and the room's link and code work while any member
may. Each join is its own input, so a seat that canInvite closes is closed
before the next join.
state in place; returning a new state fails the input. To start over, assign fresh fields to state, as again does below.For an action, data is what the member's client passed to room.act, as JSON, or undefined. Any member can
send any value, so check it and refuse what you don't accept. For a timer, data is what ctx.setTimer was given.
beat receives it: who sent it, its numbers as other members got them, and when it was sent.Properties
- fromstring
- The sender's id, as in
ctx.members. - datanumber[]
- The numbers, rounded as
room.relayrounds them. - atnumber
- When the sender's client says it sent it, on Tinylib's clock, at most 100 milliseconds later than
ctx.now. Likedata, it comes from the sender's device.
Kind's status returns for one member.Properties
- textstring
- The line Tinylib shows, such as
'Your move'. - waitingboolean
- True when the room is waiting for this member to act. Tinylib lists the rooms waiting on the user.
join receives it: { invite } with the member whose invitation they accepted, or 'link' when they used the room's link or code. A guest gets no invitations, so always joins by 'link'.leave receives it.