Rooms and rules
// rules.js
import { withViews } from 'tinylib-sdk/rules'
export const game = withViews({
address: 'game/:id',
create(options, ctx) {
return { board: Array(9).fill(''), x: ctx.by.id, o: null, next: 'x' }
},
on(state, input, ctx) {
if (input.type === 'join') {
if (state.o) ctx.refuse('This game already has two players.')
state.o = input.from.id
}
if (input.type === 'action' && input.name === 'move') {
const mark = input.from.id === state.x ? 'x' : 'o'
if (mark !== state.next) ctx.refuse("It's not your turn.")
if (state.board[input.data] !== '') ctx.refuse('Pick an empty square.')
state.board[input.data] = mark
state.next = mark === 'x' ? 'o' : 'x'
ctx.notify(mark === 'x' ? state.o : state.x, 'Your turn')
}
},
view: (state, member) => ({ board: state.board, you: member.id === state.x ? 'x' : 'o', next: state.next }),
status: (state, member) => {
const mine = (member.id === state.x ? 'x' : 'o') === state.next
return { text: mine ? 'Your turn' : 'Their turn', waiting: mine }
},
})
// app.js
const { id } = await tinylib.rooms.create('game')
const room = await tinylib.rooms.open(id)
room.onMessage(({ view }) => draw(view))
await room.send('move', 4)
A room is state several people share, run by the app's rules on Tinylib's servers. Each export of rules.js is a kind of room, named by the export. create returns the first state; on gets every input, one at a time, changes the state in place and calls ctx for outputs. A refusal or a throw throws away the change and every output of that input.
withViews sends each member { view } after every change and every time a page opens the room, and sets each member's status, the line Tinylib shows them about the room. A member sees only what the rules send them.
Opening a room
const room = await tinylib.rooms.open(id)
const stop = room.onMessage(({ view }) => draw(view))
room.onChange(() => room.ended && showEnded())
// on leaving the screen
stop()
room.close()
The persona is connected while a page has the room open on one of their devices. What the rules send reaches only members with the room open, once; messages that come before a listener is added wait for it. open rejects with not_member, ended or offline.
Refusing
try {
await room.send('move', square)
} catch (error) {
toast(error.message) // "It's not your turn."
}
ctx.refuse(sentence) stops the input, and send rejects with refused and that sentence. Creating a room, an action, a join, an invite, getting the link and deleting it can be refused. Leaving, connecting, disconnecting and opening are facts.
Inviting and joining
await tinylib.rooms.invite(id) // Tinylib's invite sheet, which also shares the link
await tinylib.rooms.invite(id, friend.id) // one persona
const { url, code } = await tinylib.rooms.getLink(id, { code: true })
const { id: joined } = await tinylib.rooms.join('K7QD')
Someone who takes an invitation or opens the link joins through Tinylib, which asks the rules with a join input. They aren't a member until the rules accept, so the rules welcome them on their connect, which follows. A kind's address, here 'game/:id', is where Tinylib sends people who come in for a room from outside the app: a notification, an invitation, the link. :id is the room's id.
A page takes the invitations it shows itself with tinylib.invitations.handle, and Tinylib shows its own card for the rest.
Members and names
on(state, input, ctx) {
const names = ctx.members.map((member) => member.name)
const here = ctx.members.filter((member) => member.connected)
}
ctx.members lists the current members with their current names, and whether each has the room open. The rules keep ids in their state, never names. Someone who left isn't in ctx.members; their leave input carries their name.
Time and randomness
on(state, input, ctx) {
if (input.type === 'action' && input.name === 'start') {
state.deadline = ctx.now + 20_000
ctx.setTimer('close', state.deadline)
state.order = state.questions.map(() => ctx.random())
}
if (input.type === 'timer' && input.name === 'close') state.phase = 'reveal'
}
const seconds = Math.ceil((view.deadline - tinylib.now()) / 1000)
ctx.now and ctx.random() come from Tinylib, so players can't rig them. A timer wakes the room with a timer input. The page counts down with tinylib.now(), on the same clock as ctx.now.
Data a room leaves
// rules.js
ctx.data.room.set('result', { winner, moves: state.moves })
// the page, during the room or after it ended
const result = await tinylib.rooms.data(id).get('result')
ctx.data.room is kept with the room's record, for its members during the room and after. A member's own results go under keys the rules write, in their data.
Ending and leaving
await tinylib.rooms.leave(id)
ctx.end() ends the room for good. Each member keeps it in their past rooms, from tinylib.rooms.list(), with the status they had at the end. A member leaving is a leave input with why, and Tinylib closes a room that goes unused for idleMs on Limits and errors.
A new publish
A publish reloads every open page of the app, and each open room gets an upgraded input before its first input under the new rules. A publish can't remove a kind that still has open rooms: its create refuses new ones, and a later publish removes it once the last has ended.