Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.deep.space/llms.txt

Use this file to discover all available pages before exploring further.

The real-time hooks beyond records and messaging - presence, collaborative editing, canvas, game rooms, and helpers.
import {
  // Yjs
  useYjsText, useYjsField, useYjsRoom,
  // Canvas
  useCanvas,
  // Presence
  usePresence, usePresenceRoom,
  // Game rooms
  useGameRoom,
  // Cron monitor
  useCronMonitor,
  // User colors
  DEFAULT_USER_COLORS, getUserColor,
  // Low-level sync primitives
  createEncoder, createDecoder, encodeSyncStep1, encodeSyncStep2, encodeUpdate,
  handleSyncMessage, Awareness, encodeAwarenessMessage, handleAwarenessMessage,
} from 'deepspace'

Yjs hooks

useYjsText(collection, recordId, fieldName)

Collaborative plain text bound to a record field.
function useYjsText(
  collection: string,
  recordId: string,
  fieldName: string,
): {
  text: string
  setText: (value: string) => void
  synced: boolean
  canWrite: boolean
}

useYjsField(collection, recordId, fieldName)

Lower-level Yjs binding. Returns the raw Y.Doc and Awareness so you can build any Yjs type (Y.Map, Y.Array, Y.XmlFragment, etc.) on top of it. Not generic - the hook doesn’t model the field shape.
function useYjsField(
  collection: string,
  recordId: string,
  fieldName: string,
): {
  doc: Y.Doc
  awareness: Awareness
  synced: boolean
  canWrite: boolean
  /** Increments on every local or remote Y.Doc update - useful as a render trigger. */
  updateCount: number
}
Build whichever Yjs type you need off doc:
const { doc, synced } = useYjsField('documents', docId, 'tasks')
const list = useMemo(() => doc.getArray<Task>('tasks'), [doc])

useYjsRoom(docId, fieldName)

Standalone Yjs document not tied to a record. Useful for ephemeral collaboration sessions. Opens a direct WebSocket to a dedicated YjsRoom DO at /ws/yjs/:docId.
function useYjsRoom(docId: string, fieldName: string): {
  doc: Y.Doc
  awareness: Awareness
  text: string
  setText: (value: string) => void
  synced: boolean
  canWrite: boolean
}
text / setText are bound to the Y.Text at fieldName. Use doc directly if you need a different type. awareness is a y-protocols Awareness instance pre-wired to the same WebSocket. Calls to awareness.setLocalState(...) or awareness.setLocalStateField('cursor' | 'selection' | 'user' | …, value) fire an MSG_AWARENESS frame to peers; remote states arrive on the awareness 'change' / 'update' events and are visible via awareness.getStates(). Pass it to an editor binding (e.g. @tiptap/extension-collaboration-cursor) or wire your own cursor/selection UI - see the collaborative editing guide for a worked example.
The /ws/yjs/:docId route is token-required and docs-aware: 401 without a verified JWT, 403 without read access. With the docs feature installed, roles resolve from documents.ownerId / editors / collaborators; without it, any authenticated caller is treated as member. See the security model for the full path.
For the raw protocol constants (MSG_AWARENESS, encodeAwarenessMessage, handleAwarenessMessage) used by this hook internally, see Low-level sync primitives below.

useCanvas(roomId)

Connects to a CanvasRoom DO.
function useCanvas(roomId: string): {
  shapes: CanvasShapeClient[]
  /** All connected users' viewports (including self). */
  viewports: ViewportClient[]
  connected: boolean
  addShape:    (shape: Partial<CanvasShapeClient>) => void
  moveShape:   (shapeId: string, x: number, y: number) => void
  resizeShape: (shapeId: string, width: number, height: number, x?: number, y?: number) => void
  updateShape: (shapeId: string, props: Record<string, unknown>) => void
  deleteShape: (shapeId: string) => void
  setViewport: (viewport: Omit<ViewportClient, 'userId'>) => void
  undo:        () => void
  redo:        () => void
}
All mutation methods are fire-and-forget - they encode and send a typed message and return void. viewports is an array, not a Map. Each shape:
type CanvasShapeClient = {
  id: string
  type: string                     // free-form: 'rect', 'circle', 'text', etc.
  x: number
  y: number
  width: number
  height: number
  rotation?: number
  props: Record<string, unknown>   // app-specific payload
  createdBy: string
  createdAt: string
  updatedAt: string
}

type ViewportClient = {
  userId: string
  x: number
  y: number
  width: number
  height: number
  zoom: number
}

Presence hooks

usePresence(options?)

Online/offline status derived from lastSeenAt heartbeats on the users collection.
function usePresence(options?: { timeoutMs?: number }): {
  users: RoomUser[]
  isOnline:    (userId: string) => boolean
  getLastSeen: (userId: string) => string | null
}
The hook also sends a heartbeat every 60 seconds so the server refreshes the caller’s lastSeenAt. Default timeoutMs is 5 minutes.

usePresenceRoom(scopeId)

High-frequency ephemeral state (cursors, typing, viewport). Connects to a dedicated PresenceRoom DO.
function usePresenceRoom(scopeId: string): {
  peers: PresencePeerClient[]   // excludes self
  connected: boolean
  updateState: (state: object) => void   // merges
}

type PresencePeerClient = {
  userId: string
  userName: string
  userEmail: string
  userImageUrl?: string
  joinedAt: string
  state: Record<string, unknown>
}
scopeId is any string. Common patterns: canvas:${canvasId}, thread:${channelId}, doc:${docId}.

useGameRoom(roomId)

Turn-tick or sim-tick game loop DO. Not generic - state is Record<string, unknown> (cast at the use-site).
function useGameRoom(roomId: string): {
  state: Record<string, unknown>
  tick: number
  players: GamePlayer[]
  running: boolean
  connected: boolean
  sendInput:  (action: string, data?: Record<string, unknown>) => void
  setReady:   () => void
  startGame:  () => void
  endGame:    () => void
}

type GamePlayer = {
  userId: string
  userName: string
  ready: boolean
  connectedAt: string
  /** Server-managed per-player slot - always present, never undefined. */
  data: Record<string, unknown>
}
All mutation methods (sendInput, setReady, startGame, endGame) are fire-and-forget - they return void. The server-side GameInput event carries userId (not playerId), action, data, and the current tick. State migration on schema bumps lives in the worker - override onHydrateState(stored) on your GameRoom subclass.

useCronMonitor(roomId)

Admin/monitor stream for the CronRoom DO. Pass app:<APP_NAME> for the app’s default cron room.
function useCronMonitor(roomId: string): {
  tasks: CronTaskState[]
  history: CronHistoryEntry[]
  connected: boolean
  trigger: (taskName: string) => void
  pause:   (taskName: string) => void
  resume:  (taskName: string) => void
}

type CronTaskState = {
  name: string
  /** Required. Null when the task is configured via `schedule` instead. */
  intervalMinutes: number | null
  /** Required. Null when the task is configured via `intervalMinutes` instead. */
  schedule: string | null
  /** Required. Null when the task has no explicit timezone. */
  timezone: string | null
  paused: boolean
  lastRunAt: string | null
  nextRunAt: string | null
}

type CronHistoryEntry = {
  taskName: string
  startedAt: string
  completedAt: string | null
  success: boolean
  durationMs: number
  error?: string
}
trigger, pause, and resume are fire-and-forget - they return void.
The Cron DO does not enforce a role on trigger / pause / resume. Gate the admin UI by useUser().user?.role === 'admin'.

User colors

// 12-color palette of cursor/avatar tints
const DEFAULT_USER_COLORS: readonly string[]

// Deterministic hash: same userId → same color
function getUserColor(userId: string, palette?: string[]): string
Use for cursor dots in usePresence / usePresenceRoom, avatar fallbacks, and “who’s typing” pills.

Low-level sync primitives

For building custom hooks against a DeepSpace Yjs DO (rare):
createEncoder, createDecoder
toUint8Array, writeVarUint, writeVarUint8Array, readVarUint, readVarUint8Array
encodeSyncStep1, encodeSyncStep2, encodeUpdate
handleSyncMessage
Awareness, encodeAwarenessMessage, handleAwarenessMessage
getMessageType
MSG_SYNC, MSG_AWARENESS, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE
Most apps never use these directly. Use the higher-level hooks (useYjsText, etc.) unless you’re building a custom binding.

See also