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