Files
base/sdk/base-react/src/use-realtime.ts
zeekay 7f17d4b316 Add realtime DX platform: CRDT engine, WebSocket, SDKs, typegen, scheduler, multi-tenant
7-phase implementation of Convex-inspired realtime developer experience:

- crdt/: Go CRDT engine (GCounter, PNCounter, LWWRegister, ORSet, RGA text)
  with state-vector sync protocol and convergence-tested types (31 tests)
- apis/ws.go: Pure RFC 6455 WebSocket endpoint with JWT auth, CRDT sync,
  presence tracking, channel subscriptions
- apis/presence.go: Channel-based presence manager for collaborative awareness
- cmd/typegen/: CLI tool to generate TypeScript interfaces from Base schemas
  (4 tests, all 14 field types mapped)
- plugins/scheduler/: Scheduled functions with exactly-once semantics,
  retry with backoff, JSVM bindings (8 tests)
- plugins/platform/: Multi-tenant platform with IAM (hanzo.id) auth,
  KMS (kms.hanzo.ai) secrets, tenant isolation via collection prefixes,
  RBAC roles, API key management (7 tests)
- sdk/base-js/: @hanzoai/base TypeScript SDK with reactive queries,
  query deduplication, optimistic updates, state versioning, CRDT client
  (72 smoke tests passing)
- sdk/base-react/: @hanzoai/base-react React hooks (useQuery, useMutation,
  useAuth, useRealtime, useCRDT, usePresence) with useSyncExternalStore
2026-02-27 10:00:24 -08:00

73 lines
2.2 KiB
TypeScript

/**
* useRealtime -- low-level SSE subscription hook.
*
* Subscribes to realtime events for a specific collection and optional
* record id. The callback fires on every create/update/delete event.
*
* Uses the RealtimeService's built-in ref-counting: the SSE connection
* is shared across all hooks subscribing to the same collection, and
* auto-disconnects when the last subscriber unmounts.
*/
import { useEffect, useRef } from 'react'
import type { RealtimeEvent, ConnectionState } from '@hanzoai/base'
import { useBaseClient } from './provider.js'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type RealtimeHandler = (event: RealtimeEvent) => void
export interface UseRealtimeOptions {
/** Record id to subscribe to. Default "*" (all records). */
recordId?: string
/** Disable the subscription. Default false. */
enabled?: boolean
/** Connection state change callback. */
onConnectionChange?: (state: ConnectionState) => void
}
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
export function useRealtime(
collection: string,
callback: RealtimeHandler,
options?: UseRealtimeOptions,
): void {
const client = useBaseClient()
const topic = options?.recordId ?? '*'
const enabled = options?.enabled !== false
// Keep callback ref stable to avoid re-subscribing on every render.
const callbackRef = useRef(callback)
callbackRef.current = callback
// SSE subscription.
useEffect(() => {
if (!enabled) return
const unsub = client.realtime.subscribe(collection, topic, (event) => {
callbackRef.current(event)
})
return unsub
}, [client, collection, topic, enabled])
// Connection state listener.
const onConnectionChangeRef = useRef(options?.onConnectionChange)
onConnectionChangeRef.current = options?.onConnectionChange
useEffect(() => {
if (!enabled || !onConnectionChangeRef.current) return
const unsub = client.realtime.onConnectionChange((state) => {
onConnectionChangeRef.current?.(state)
})
return unsub
}, [client, enabled])
}