import { AppState } from 'react-native' /** * Realtime policy for the mobile Knowledge screen. * * Supabase Postgres Changes does not apply RLS to DELETE events and cannot * filter them, so an unfiltered DELETE subscription would deliver every * account's deleted primary keys to every subscriber and make each client * refetch on every deletion anywhere (N-client fan-out). Only INSERT/UPDATE are * subscribed (RLS-scoped, optionally narrowed to the owner), and deletions are * reconciled by foreground polling, mirroring use-history-sync.ts. */ export type KnowledgeRealtimeEvent = 'INSERT' | 'UPDATE' export interface KnowledgeRealtimeBinding { event: KnowledgeRealtimeEvent schema: 'public' table: 'knowledge_documents' filter?: string } export const KNOWLEDGE_DELETION_RECONCILE_INTERVAL_MS = 45_000 const SUBSCRIBED_EVENTS: readonly KnowledgeRealtimeEvent[] = ['INSERT', 'UPDATE'] /** * Pure: the postgres_changes bindings the Knowledge screen may open. DELETE is * intentionally never included. `ownerId` must already be validated by the * caller because it is interpolated into the realtime filter. */ export function knowledgeRealtimeBindings(ownerId?: string): KnowledgeRealtimeBinding[] { return SUBSCRIBED_EVENTS.map((event) => ({ event, schema: 'public', table: 'knowledge_documents', ...(ownerId === undefined ? {} : { filter: `user_id=eq.${ownerId}` }), })) } /** Port: whether the app is in the foreground, and changes to that state. */ export interface ForegroundStatePort { isActive: () => boolean onChange: (listener: (active: boolean) => void) => { remove: () => void } } /** Port: interval scheduling, injectable for tests. */ export interface IntervalPort { set: (callback: () => void, ms: number) => ReturnType clear: (handle: ReturnType) => void } export interface ForegroundReconciliationDeps { foreground?: ForegroundStatePort interval?: IntervalPort intervalMs?: number } export interface ForegroundReconciliation { stop: () => void } export const reactNativeForegroundState: ForegroundStatePort = { isActive: () => AppState.currentState === 'active', onChange: (listener) => { const subscription = AppState.addEventListener('change', (state) => { listener(state === 'active') }) return { remove: () => subscription.remove() } }, } const systemInterval: IntervalPort = { set: (callback, ms) => setInterval(callback, ms), clear: (handle) => clearInterval(handle), } /** * Calls `onReconcile` periodically while the app is in the foreground and once * whenever it returns to the foreground, so remote deletions are picked up * without a DELETE realtime subscription. */ export function startForegroundReconciliation( onReconcile: () => void, deps: ForegroundReconciliationDeps = {}, ): ForegroundReconciliation { const foreground = deps.foreground ?? reactNativeForegroundState const interval = deps.interval ?? systemInterval const intervalMs = deps.intervalMs ?? KNOWLEDGE_DELETION_RECONCILE_INTERVAL_MS let stopped = false let active = foreground.isActive() const stateSubscription = foreground.onChange((nextActive) => { const resumed = nextActive && !active active = nextActive if (resumed && !stopped) onReconcile() }) const handle = interval.set(() => { if (active && !stopped) onReconcile() }, intervalMs) return { stop: () => { if (stopped) return stopped = true interval.clear(handle) stateSubscription.remove() }, } }