mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
- presence.go: Replace slice-index callback tracking with ID-based map to prevent stale index corruption on unsubscribe - ws.go: Remove 7 unused WebSocket close code constants - platform.go: Move config defaults before IAMClient creation, use cached p.iam.ValidateToken() instead of standalone function - kms.go: Add KMS auth token caching with expiry-aware refresh - collection.ts: Implement unsubscribe() (was silent no-op) - index.ts: Remove duplicate ListResult type export - use-query.ts: Fix missing deps bug by serializing full options as stable key via JSON.stringify, remove eslint-disable
145 lines
4.3 KiB
TypeScript
145 lines
4.3 KiB
TypeScript
/**
|
|
* useQuery -- reactive collection query with SSE sync.
|
|
*
|
|
* Subscribes to the QueryStore for a (collection, filter) pair using
|
|
* useSyncExternalStore. Automatically fetches on mount and subscribes
|
|
* to realtime updates via subscribeAndSync.
|
|
*
|
|
* Multiple components using the same (collection, filter) share one
|
|
* SSE connection through the RealtimeService ref-counting.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
|
import type { BaseRecord, ListOptions } from '@hanzoai/base'
|
|
import { useBaseClient } from './provider.js'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface UseQueryOptions extends ListOptions {
|
|
/** Disable automatic fetching. Default false. */
|
|
enabled?: boolean
|
|
/** Subscribe to realtime updates. Default true. */
|
|
realtime?: boolean
|
|
}
|
|
|
|
export interface UseQueryResult<T extends BaseRecord = BaseRecord> {
|
|
data: T[]
|
|
isLoading: boolean
|
|
error: Error | null
|
|
refetch: () => Promise<void>
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Hook
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function useQuery<T extends BaseRecord = BaseRecord>(
|
|
collection: string,
|
|
options?: UseQueryOptions,
|
|
): UseQueryResult<T> {
|
|
const client = useBaseClient()
|
|
const filter = options?.filter ?? ''
|
|
const enabled = options?.enabled !== false
|
|
const realtimeEnabled = options?.realtime !== false
|
|
|
|
// Stable serialization of options for dependency tracking.
|
|
const optionsKey = useMemo(() => {
|
|
if (!options) return ''
|
|
const { enabled: _e, realtime: _r, ...rest } = options
|
|
return JSON.stringify(rest)
|
|
}, [options])
|
|
|
|
// Stable reference for the current snapshot from the store.
|
|
const snapshotRef = useRef<T[]>(
|
|
(client.store.getQuery(collection, filter) as T[] | undefined) ?? [],
|
|
)
|
|
const errorRef = useRef<Error | null>(null)
|
|
const loadingRef = useRef(enabled)
|
|
|
|
// Subscribe to QueryStore changes via useSyncExternalStore.
|
|
const subscribe = useCallback(
|
|
(onStoreChange: () => void) => {
|
|
return client.store.subscribe(collection, filter, (records) => {
|
|
snapshotRef.current = records as T[]
|
|
onStoreChange()
|
|
})
|
|
},
|
|
[client, collection, filter],
|
|
)
|
|
|
|
const getSnapshot = useCallback(() => {
|
|
return snapshotRef.current
|
|
}, [])
|
|
|
|
const data = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
|
|
// Track loading state with a separate subscription.
|
|
const loadingSubscribe = useCallback(
|
|
(onStoreChange: () => void) => {
|
|
// Loading state is managed internally; we piggyback on the store
|
|
// subscription to trigger re-renders when loading changes.
|
|
return client.store.subscribe(collection, filter, () => {
|
|
if (loadingRef.current) {
|
|
loadingRef.current = false
|
|
onStoreChange()
|
|
}
|
|
})
|
|
},
|
|
[client, collection, filter],
|
|
)
|
|
|
|
const getLoadingSnapshot = useCallback(() => {
|
|
return loadingRef.current
|
|
}, [])
|
|
|
|
const isLoading = useSyncExternalStore(
|
|
loadingSubscribe,
|
|
getLoadingSnapshot,
|
|
getLoadingSnapshot,
|
|
)
|
|
|
|
// Fetch function.
|
|
const refetch = useCallback(async () => {
|
|
loadingRef.current = true
|
|
errorRef.current = null
|
|
try {
|
|
await client.list(collection, options)
|
|
loadingRef.current = false
|
|
} catch (err) {
|
|
errorRef.current = err instanceof Error ? err : new Error(String(err))
|
|
loadingRef.current = false
|
|
}
|
|
}, [client, collection, optionsKey])
|
|
|
|
// Initial fetch on mount and when options change.
|
|
useEffect(() => {
|
|
if (!enabled) return
|
|
refetch()
|
|
}, [enabled, collection, optionsKey, refetch])
|
|
|
|
// Realtime subscription (ref-counted via subscribeAndSync).
|
|
useEffect(() => {
|
|
if (!enabled || !realtimeEnabled) return
|
|
const unsub = client.subscribeAndSync(collection)
|
|
return unsub
|
|
}, [client, collection, enabled, realtimeEnabled])
|
|
|
|
// Auto-resubscribe on auth change.
|
|
useEffect(() => {
|
|
if (!enabled) return
|
|
const unsub = client.authStore.onChange(() => {
|
|
refetch()
|
|
})
|
|
return unsub
|
|
}, [client, enabled, refetch])
|
|
|
|
return {
|
|
data,
|
|
isLoading,
|
|
error: errorRef.current,
|
|
refetch,
|
|
}
|
|
}
|