mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
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
73 lines
2.2 KiB
TypeScript
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])
|
|
}
|