Models

Model files attach methods to paths in the TeamPlay data tree. A model is a Signal subclass.

Collection Models

models/users/index.ts is the collection model. It receives the collection signal, so it should extend Signal<User[]>.

// models/users/index.ts
import { Signal } from 'teamplay'
import type User from './schema.ts'

export default class UsersModel extends Signal<User[]> {
  async addNew (user: Omit<User, 'createdAt'>) {
    return await this.add({
      ...user,
      createdAt: Date.now()
    })
  }
}

Collection methods are a good place for create helpers, default fields, and collection-level workflows.

After initialization, collection methods are available from $:

const userId = await $.users.addNew({ name: 'Ada' })

Document Models

models/users/[id].ts is the document model. It receives one document signal, so it should extend Signal<User>.

// models/users/[id].ts
import { Signal } from 'teamplay'
import type User from './schema.ts'

export default class UserModel extends Signal<User> {
  displayName () {
    return this.name.get()
  }

  async rename (name: string) {
    await this.name.set(name)
  }
}

Document methods are useful for business operations that belong to one document:

const $user = await sub($.users[userId])

$user.displayName()
await $user.rename('Ada Lovelace')

For simple updates, use signal methods directly:

await $.users[userId].name.set('Ada')
await $.users[userId].assign({ email: 'ada@example.com' })

Nested Models

Nested model files attach methods below a document:

models/games/[id]/players/[playerId].ts -> games.*.players.*
// models/games/[id]/players/[playerId].ts
import { Signal } from 'teamplay'
import type Game from '../../schema.ts'

type GamePlayer = Game['players'][number]

export default class GamePlayerModel extends Signal<GamePlayer> {
  displayName () {
    return this.robot.get() ? `${this.name.get()} (bot)` : this.name.get()
  }
}

Now nested methods are available on matching paths:

$.games[gameId].players[0].displayName()

Subscribe Before Reading

Always subscribe to database data before reading it:

const $user = await sub($.users[userId])
$user.displayName()

In React, use useSub():

import { $, observer, useSub } from 'teamplay'

export default observer(function UserName ({ userId }: { userId: string }) {
  const $user = useSub($.users[userId])
  return $user.displayName()
})

Local/private signals such as $._session do not need subscriptions.