@@ -36,33 +36,57 @@ export interface UseSyncStreamOptions extends SyncStreamSubscribeOptions {
3636 * @returns The status for that stream, or `null` if the stream is currently being resolved.
3737 */
3838export function useSyncStream ( options : UseSyncStreamOptions ) : SyncStreamStatus | null {
39- const { name, parameters } = options ;
39+ return useSyncStreams ( [ options ] ) [ 0 ] ;
40+ }
41+
42+ /**
43+ * Creates multiple PowerSync stream subscriptions. Subscriptions are kept alive as long as the
44+ * React component calling this function. When it unmounts, or when the streams array contents
45+ * change, all previous subscriptions are unsubscribed before new ones are created.
46+ */
47+ export function useSyncStreams ( streamOptions : UseSyncStreamOptions [ ] ) : SyncStreamStatus [ ] {
4048 const db = usePowerSync ( ) ;
4149 const status = useStatus ( ) ;
42- const stream = useMemo ( ( ) => db . syncStream ( name , parameters ) , [ name , parameters ] ) ;
50+
51+ const stringifiedOptions = useMemo ( ( ) => JSON . stringify ( streamOptions ) , [ streamOptions ] ) ;
52+ const syncStreams = useMemo (
53+ ( ) =>
54+ streamOptions . map ( ( options ) => {
55+ return {
56+ stream : db . syncStream ( options . name , options . parameters ?? undefined ) ,
57+ options
58+ } ;
59+ } ) ,
60+ [ stringifiedOptions ]
61+ ) ;
4362
4463 useEffect ( ( ) => {
4564 let active = true ;
46- let subscription : SyncStreamSubscription | null = null ;
65+ const resolvedSubs : SyncStreamSubscription [ ] = [ ] ;
4766
48- stream . subscribe ( options ) . then ( ( sub ) => {
49- if ( active ) {
50- subscription = sub ;
51- } else {
52- // The cleanup function already ran, unsubscribe immediately.
53- sub . unsubscribe ( ) ;
54- }
55- } ) ;
67+ for ( const entry of syncStreams ) {
68+ entry . stream . subscribe ( entry . options ) . then ( ( sub ) => {
69+ if ( active ) {
70+ resolvedSubs . push ( sub ) ;
71+ } else {
72+ // The cleanup function already ran, unsubscribe immediately.
73+ sub . unsubscribe ( ) ;
74+ }
75+ } ) ;
76+ }
5677
5778 return ( ) => {
5879 active = false ;
59- // If we don't have a subscription yet, it'll still get cleaned up once the promise resolves because we've set
60- // active to false.
61- subscription ?. unsubscribe ( ) ;
80+ for ( const sub of resolvedSubs ) {
81+ sub . unsubscribe ( ) ;
82+ }
6283 } ;
63- } , [ stream ] ) ;
84+ } , [ stringifiedOptions ] ) ;
6485
65- return status . forStream ( stream ) ?? null ;
86+ return useMemo (
87+ ( ) => syncStreams . map ( ( entry ) => status . forStream ( entry . stream ) ?? null ) ,
88+ [ status , stringifiedOptions ]
89+ ) ;
6690}
6791
6892/**
@@ -72,57 +96,12 @@ export function useAllSyncStreamsHaveSynced(
7296 db : AbstractPowerSyncDatabase ,
7397 streams : QuerySyncStreamOptions [ ] | undefined
7498) : boolean {
75- // Since streams are a user-supplied array, they will likely be different each time this function is called. We don't
76- // want to update underlying subscriptions each time, though.
77- const hash = useMemo ( ( ) => streams && JSON . stringify ( streams ) , [ streams ] ) ;
78- const [ synced , setHasSynced ] = useState ( streams == null || streams . every ( ( e ) => e . waitForStream != true ) ) ;
99+ const statuses = useSyncStreams ( streams ?? [ ] ) ;
79100
80- useEffect ( ( ) => {
81- if ( streams ) {
82- setHasSynced ( false ) ;
83-
84- const promises : Promise < SyncStreamSubscription > [ ] = [ ] ;
85- const abort = new AbortController ( ) ;
86- for ( const stream of streams ) {
87- promises . push ( db . syncStream ( stream . name , stream . parameters ) . subscribe ( stream ) ) ;
88- }
89-
90- // First, wait for all subscribe() calls to make all subscriptions active.
91- Promise . all ( promises ) . then ( async ( resolvedStreams ) => {
92- function allHaveSynced ( status : SyncStatus ) {
93- return resolvedStreams . every ( ( s , i ) => {
94- const request = streams [ i ] ;
95- return ! request . waitForStream || status . forStream ( s ) ?. subscription ?. hasSynced ;
96- } ) ;
97- }
98-
99- // Wait for the effect to be cancelled or all streams having synced.
100- await db . waitForStatus ( allHaveSynced , abort . signal ) ;
101- if ( abort . signal . aborted ) {
102- // Was cancelled
103- } else {
104- // Has synced, update public state.
105- setHasSynced ( true ) ;
106-
107- // Wait for cancellation before clearing subscriptions.
108- await new Promise < void > ( ( resolve ) => {
109- abort . signal . addEventListener ( 'abort' , ( ) => resolve ( ) ) ;
110- } ) ;
111- }
112-
113- // Effect was definitely cancelled at this point, so drop the subscriptions.
114- for ( const stream of resolvedStreams ) {
115- stream . unsubscribe ( ) ;
116- }
117- } ) ;
118-
119- return ( ) => abort . abort ( ) ;
120- } else {
121- // There are no streams, so all of them have synced.
122- setHasSynced ( true ) ;
123- return undefined ;
124- }
125- } , [ hash ] ) ;
101+ if ( ! streams ) return true ;
126102
127- return synced ;
103+ return streams . every ( ( stream , i ) => {
104+ if ( ! stream . waitForStream ) return true ;
105+ return statuses [ i ] ?. subscription ?. hasSynced === true ;
106+ } ) ;
128107}
0 commit comments