// 本地库变更订阅 (任务模块即时同步, 见 docs/V0.2/离线编辑模块/即时同步方案.md) // // 作用: 当指定表在本地 SQLite 里发生变化时回调 —— 变化可能来自 // 1) 本端写入 (乐观更新之后) // 2) PowerSync 下行推送 (其它端 / 服务端改动) // 两者最终都落到本地库, 所以订阅本地库即可覆盖跨端场景, 且离线时同样有效。 // // 为什么不用手写"信号 SQL"求和: SDK 的 watch 已支持按表名过滤 (tables) // + 节流 (throttleMs), 由 SDK 判断是否真的变化, 比自己做 count/max 汇总可靠。 // // 用法: // useDbSignal(['Task', 'BoardColumn'], () => load()) // useDbSignal(TASK_TABLES, onChange, { throttleMs: 300, enabled: someFlag }) import { useEffect, useRef } from 'react' import { getPowersyncDb } from './db.js' // 无意义的占位查询: 只借 watch 的"变更通知"能力, 不关心结果 const SIGNAL_SQL = 'SELECT 1 AS s' export default function useDbSignal(tables, onChange, options = {}) { const { throttleMs = 250, enabled = true } = options // 回调走 ref, 避免调用方每次渲染都重新订阅 const cbRef = useRef(onChange) cbRef.current = onChange // 表名数组每帧都是新引用, 用内容做依赖 const tablesKey = Array.isArray(tables) ? tables.join(',') : '' useEffect(() => { // 回滚开关: localStorage['iboard_realtime'] = '0' 时完全不订阅, 退回"手动刷新"行为, // 用于线上出问题时的快速止损 (改完需重启应用生效) let realtimeOff = false try { realtimeOff = localStorage.getItem('iboard_realtime') === '0' } catch { /* ignore */ } if (!enabled || !tablesKey || realtimeOff) return undefined const db = getPowersyncDb() const ac = new AbortController() let disposed = false // watch 订阅成功后会立刻给出一次当前结果, 而调用方挂载时已经主动 load 过, // 这里跳过首次通知, 避免重复请求 let first = true try { db.watch( SIGNAL_SQL, [], { onResult: () => { if (disposed) return if (first) { first = false; return } cbRef.current?.() }, onError: (e) => { if (!disposed) console.warn('[db-signal] watch 出错:', e) }, }, { tables: tablesKey.split(','), throttleMs, signal: ac.signal }, ) } catch (e) { // 订阅失败不应影响页面主流程 (退化为"手动刷新") console.warn('[db-signal] 订阅失败:', e) } return () => { disposed = true ac.abort() } }, [tablesKey, throttleMs, enabled]) }