@@ -4,7 +4,7 @@ import React, { act, useSyncExternalStore } from 'react';
44import { AbstractPowerSyncDatabase , ConnectionManager , SyncStatus } from '@powersync/common' ;
55import { openPowerSync } from './utils' ;
66import { PowerSyncContext } from '../src/hooks/PowerSyncContext' ;
7- import { useSyncStream , UseSyncStreamOptions } from '../src/hooks/streams' ;
7+ import { useSyncStream , useSyncStreams , UseSyncStreamOptions } from '../src/hooks/streams' ;
88import { useQuery } from '../src/hooks/watched/useQuery' ;
99import { QuerySyncStreamOptions } from '../src/hooks/watched/watch-types' ;
1010
@@ -142,6 +142,102 @@ describe('stream hooks', () => {
142142 await waitFor ( ( ) => expect ( currentStreams ( ) ) . toHaveLength ( 0 ) , { timeout : 1000 , interval : 100 } ) ;
143143 } ) ;
144144
145+ it ( 'useSyncStreams subscribes and unsubscribes' , async ( ) => {
146+ expect ( currentStreams ( ) ) . toStrictEqual ( [ ] ) ;
147+
148+ const { result, unmount } = renderHook ( ( ) => useSyncStreams ( [ { name : 'a' } , { name : 'b' } ] ) , {
149+ wrapper : testWrapper
150+ } ) ;
151+ expect ( result . current ) . toStrictEqual ( [ null , null ] ) ;
152+ await waitFor ( ( ) => expect ( currentStreams ( ) ) . toHaveLength ( 2 ) , { timeout : 1000 , interval : 100 } ) ;
153+ await waitFor ( ( ) => expect ( result . current . every ( ( s ) => s !== null ) ) . toBe ( true ) , {
154+ timeout : 1000 ,
155+ interval : 100
156+ } ) ;
157+
158+ unmount ( ) ;
159+ expect ( currentStreams ( ) ) . toStrictEqual ( [ ] ) ;
160+ } ) ;
161+
162+ it ( 'useSyncStreams with cached instance' , async ( ) => {
163+ const existingSubscription = await db . syncStream ( 'a' ) . subscribe ( ) ;
164+ await existingSubscription . unsubscribe ( ) ;
165+
166+ const { result } = renderHook ( ( ) => useSyncStreams ( [ { name : 'a' } ] ) , {
167+ wrapper : testWrapper
168+ } ) ;
169+ expect ( result . current [ 0 ] ) . not . toBeNull ( ) ;
170+ } ) ;
171+
172+ it ( 'useSyncStreams handles array growing from 1 to 2 entries' , async ( ) => {
173+ let streamOptions : UseSyncStreamOptions [ ] = [ { name : 'a' } ] ;
174+ let streamUpdateListeners : ( ( ) => void ) [ ] = [ ] ;
175+
176+ const { result } = renderHook (
177+ ( ) => {
178+ const options = useSyncExternalStore (
179+ ( cb ) => {
180+ streamUpdateListeners . push ( cb ) ;
181+ return ( ) => {
182+ const index = streamUpdateListeners . indexOf ( cb ) ;
183+ if ( index != - 1 ) {
184+ streamUpdateListeners . splice ( index , 1 ) ;
185+ }
186+ } ;
187+ } ,
188+ ( ) => streamOptions
189+ ) ;
190+ return useSyncStreams ( options ) ;
191+ } ,
192+ { wrapper : testWrapper }
193+ ) ;
194+
195+ await waitFor ( ( ) => expect ( currentStreams ( ) ) . toHaveLength ( 1 ) , { timeout : 1000 , interval : 100 } ) ;
196+
197+ // Grow to 2 entries
198+ streamOptions = [ { name : 'a' } , { name : 'b' } ] ;
199+ act ( ( ) => streamUpdateListeners . forEach ( ( cb ) => cb ( ) ) ) ;
200+
201+ await waitFor ( ( ) => expect ( currentStreams ( ) ) . toHaveLength ( 2 ) , { timeout : 1000 , interval : 100 } ) ;
202+ expect ( result . current ) . toHaveLength ( 2 ) ;
203+ } ) ;
204+
205+ it ( 'useSyncStreams handles array shrinking from 2 to 1 entries' , async ( ) => {
206+ let streamOptions : UseSyncStreamOptions [ ] = [ { name : 'a' } , { name : 'b' } ] ;
207+ let streamUpdateListeners : ( ( ) => void ) [ ] = [ ] ;
208+
209+ const { result, unmount } = renderHook (
210+ ( ) => {
211+ const options = useSyncExternalStore (
212+ ( cb ) => {
213+ streamUpdateListeners . push ( cb ) ;
214+ return ( ) => {
215+ const index = streamUpdateListeners . indexOf ( cb ) ;
216+ if ( index != - 1 ) {
217+ streamUpdateListeners . splice ( index , 1 ) ;
218+ }
219+ } ;
220+ } ,
221+ ( ) => streamOptions
222+ ) ;
223+ return useSyncStreams ( options ) ;
224+ } ,
225+ { wrapper : testWrapper }
226+ ) ;
227+
228+ await waitFor ( ( ) => expect ( currentStreams ( ) ) . toHaveLength ( 2 ) , { timeout : 1000 , interval : 100 } ) ;
229+
230+ // Shrink to 1 entry
231+ streamOptions = [ { name : 'a' } ] ;
232+ act ( ( ) => streamUpdateListeners . forEach ( ( cb ) => cb ( ) ) ) ;
233+
234+ await waitFor ( ( ) => expect ( currentStreams ( ) ) . toHaveLength ( 1 ) , { timeout : 1000 , interval : 100 } ) ;
235+ expect ( result . current ) . toHaveLength ( 1 ) ;
236+
237+ unmount ( ) ;
238+ expect ( currentStreams ( ) ) . toStrictEqual ( [ ] ) ;
239+ } ) ;
240+
145241 it ( 'handles stream parameter changes' , async ( ) => {
146242 // Start without streams
147243 let streams : QuerySyncStreamOptions [ ] = [ ] ;
0 commit comments