@@ -10,39 +10,39 @@ export function useSyncStreams(streams: UseSyncStreamOptions[]) {
1010 const db = usePowerSync ( ) ;
1111 const status = useStatus ( ) ;
1212
13- // Serialize streams so the effect only re-runs when content actually changes.
14- // We also parse it back so the effect closure uses the EXACT same streams that triggered it —
15- // avoiding the stale-ref problem where streamsRef.current may have advanced to a newer render
16- // by the time the effect flushes.
13+ // Serialize to a string so the effect dep is a stable primitive.
14+ // Parsed back inside the effect so the closure always uses the exact snapshot for this run.
1715 const serialized = useMemo ( ( ) => JSON . stringify ( streams ) , [ streams ] ) ;
18- const frozenStreams = useMemo < UseSyncStreamOptions [ ] > ( ( ) => JSON . parse ( serialized ) , [ serialized ] ) ;
1916
2017 useEffect ( ( ) => {
21- const abort = new AbortController ( ) ;
22-
23- const promises = frozenStreams . map ( ( options ) =>
24- db . syncStream ( options . name , options . parameters ?? undefined ) . subscribe ( options )
25- ) ;
26-
27- Promise . all ( promises ) . then ( ( resolvedSubs ) => {
28- if ( abort . signal . aborted ) {
29- // Cleanup already ran before all promises resolved — unsubscribe immediately.
30- for ( const sub of resolvedSubs ) {
18+ const currentStreams : UseSyncStreamOptions [ ] = JSON . parse ( serialized ) ;
19+
20+ // `cleanedUp` is set synchronously when the cleanup function runs.
21+ // The async loop checks it after each await so any handle that resolves
22+ // after cleanup is unsubscribed immediately rather than being orphaned.
23+ let cleanedUp = false ;
24+ const resolvedSubs : { unsubscribe ( ) : void } [ ] = [ ] ;
25+
26+ ( async ( ) => {
27+ for ( const options of currentStreams ) {
28+ if ( cleanedUp ) break ;
29+ const sub = await db . syncStream ( options . name , options . parameters ?? undefined ) . subscribe ( options ) ;
30+ if ( cleanedUp ) {
31+ // Cleanup already ran while this subscribe was in flight — drop it immediately.
3132 sub . unsubscribe ( ) ;
33+ break ;
3234 }
33- return ;
35+ resolvedSubs . push ( sub ) ;
3436 }
37+ } ) ( ) ;
3538
36- // Cleanup will run eventually — unsubscribe when it does.
37- abort . signal . addEventListener ( 'abort' , ( ) => {
38- for ( const sub of resolvedSubs ) {
39- sub . unsubscribe ( ) ;
40- }
41- } ) ;
42- } ) ;
43-
44- return ( ) => abort . abort ( ) ;
45- } , [ frozenStreams ] ) ;
39+ return ( ) => {
40+ cleanedUp = true ;
41+ for ( const sub of resolvedSubs ) {
42+ sub . unsubscribe ( ) ;
43+ }
44+ } ;
45+ } , [ serialized ] ) ;
4646
4747 return useMemo (
4848 ( ) =>
0 commit comments