@@ -102,6 +102,7 @@ public class PowerSyncDatabase : EventStream<PowerSyncDBEvent>, IPowerSyncDataba
102102 protected IBucketStorageAdapter BucketStorageAdapter ;
103103
104104 protected CancellationTokenSource ? syncStreamStatusCts ;
105+ protected CancellationTokenSource watchSubscriptionCts = new ( ) ;
105106
106107 protected SyncStatus CurrentStatus ;
107108
@@ -365,6 +366,16 @@ public async Task Connect(IPowerSyncBackendConnector connector, PowerSyncConnect
365366 await syncStreamImplementation . Connect ( options ) ;
366367 }
367368
369+ /// <summary>
370+ /// Unsubscribe from all currently watched queries.
371+ /// </summary>
372+ protected void UnsubscribeAllQueries ( )
373+ {
374+ watchSubscriptionCts . Cancel ( ) ;
375+ watchSubscriptionCts . Dispose ( ) ;
376+ watchSubscriptionCts = new ( ) ;
377+ }
378+
368379 public async Task Disconnect ( )
369380 {
370381 await WaitForReady ( ) ;
@@ -415,7 +426,7 @@ await Database.WriteTransaction(async tx =>
415426
416427 if ( Closed ) return ;
417428
418-
429+ UnsubscribeAllQueries ( ) ;
419430 await Disconnect ( ) ;
420431 base . Close ( ) ;
421432 syncStreamImplementation ? . Close ( ) ;
@@ -671,64 +682,61 @@ public async Task<T> WriteTransaction<T>(Func<ITransaction, Task<T>> fn, DBLockO
671682 /// Use <see cref="SQLWatchOptions.ThrottleMs"/> to specify the minimum interval between queries.
672683 /// Source tables are automatically detected using <c>EXPLAIN QUERY PLAN</c>.
673684 /// </summary>
674- public Task Watch < T > ( string query , object ? [ ] ? parameters , WatchHandler < T > handler , SQLWatchOptions ? options = null )
675- => WatchInternal ( query , parameters , handler , options , GetAll < T > ) ;
685+ public Task < IDisposable > Watch < T > ( string query , object ? [ ] ? parameters , WatchHandler < T > handler , SQLWatchOptions ? options = null )
686+ => Task . Run ( ( ) => WatchInternal ( query , parameters , handler , options , GetAll < T > ) ) ;
676687
677688 /// <summary>
678689 /// Executes a read query every time the source tables are modified.
679690 /// <para />
680691 /// Use <see cref="SQLWatchOptions.ThrottleMs"/> to specify the minimum interval between queries.
681692 /// Source tables are automatically detected using <c>EXPLAIN QUERY PLAN</c>.
682693 /// </summary>
683- public Task Watch ( string query , object ? [ ] ? parameters , WatchHandler < dynamic > handler , SQLWatchOptions ? options = null )
684- => WatchInternal ( query , parameters , handler , options , GetAll ) ;
694+ public Task < IDisposable > Watch ( string query , object ? [ ] ? parameters , WatchHandler < dynamic > handler , SQLWatchOptions ? options = null )
695+ => Task . Run ( ( ) => WatchInternal ( query , parameters , handler , options , GetAll ) ) ;
685696
686- private Task WatchInternal < T > (
697+ private async Task < IDisposable > WatchInternal < T > (
687698 string query ,
688699 object ? [ ] ? parameters ,
689700 WatchHandler < T > handler ,
690701 SQLWatchOptions ? options ,
691702 Func < string , object ? [ ] ? , Task < T [ ] > > getter
692703 )
693704 {
694- var tcs = new TaskCompletionSource < bool > ( ) ;
695- Task . Run ( async ( ) =>
705+ try
696706 {
697- try
698- {
699- var resolvedTables = await ResolveTables ( query , parameters , options ) ;
700- var result = await getter ( query , parameters ) ;
701- handler . OnResult ( result ) ;
707+ var resolvedTables = await ResolveTables ( query , parameters , options ) ;
708+ var result = await getter ( query , parameters ) ;
709+ handler . OnResult ( result ) ;
702710
703- OnChange ( new WatchOnChangeHandler
711+ var subscription = OnChange ( new WatchOnChangeHandler
712+ {
713+ OnChange = async ( change ) =>
704714 {
705- OnChange = async ( change ) =>
715+ try
706716 {
707- try
708- {
709- var result = await getter ( query , parameters ) ;
710- handler . OnResult ( result ) ;
711- }
712- catch ( Exception ex )
713- {
714- handler . OnError ? . Invoke ( ex ) ;
715- }
716- } ,
717- OnError = handler . OnError
718- } , new SQLWatchOptions
719- {
720- Tables = resolvedTables ,
721- Signal = options ? . Signal ,
722- ThrottleMs = options ? . ThrottleMs
723- } ) ;
724- tcs . SetResult ( true ) ;
725- }
726- catch ( Exception ex )
717+ var result = await getter ( query , parameters ) ;
718+ handler . OnResult ( result ) ;
719+ }
720+ catch ( Exception ex )
721+ {
722+ handler . OnError ? . Invoke ( ex ) ;
723+ }
724+ } ,
725+ OnError = handler . OnError
726+ } , new SQLWatchOptions
727727 {
728- handler . OnError ? . Invoke ( ex ) ;
729- }
730- } ) ;
731- return tcs . Task ;
728+ Tables = resolvedTables ,
729+ Signal = options ? . Signal ,
730+ ThrottleMs = options ? . ThrottleMs
731+ } ) ;
732+
733+ return subscription ;
734+ }
735+ catch ( Exception ex )
736+ {
737+ handler . OnError ? . Invoke ( ex ) ;
738+ throw ;
739+ }
732740 }
733741
734742 private class ExplainedResult
@@ -776,7 +784,7 @@ public async Task<string[]> ResolveTables(string sql, object?[]? parameters = nu
776784 /// This is preferred over <see cref="Watch"/> when multiple queries need to be performed
777785 /// together in response to data changes.
778786 /// </summary>
779- public void OnChange ( WatchOnChangeHandler handler , SQLWatchOptions ? options = null )
787+ public IDisposable OnChange ( WatchOnChangeHandler handler , SQLWatchOptions ? options = null )
780788 {
781789 var resolvedOptions = options ?? new SQLWatchOptions ( ) ;
782790
@@ -811,13 +819,27 @@ void flushTableUpdates()
811819 }
812820 } ) ;
813821
822+ CancellationTokenSource linkedCts ;
814823 if ( options ? . Signal . HasValue == true )
815824 {
816- options . Signal . Value . Register ( ( ) =>
817- {
818- cts . Cancel ( ) ;
819- } ) ;
825+ // Cancel on global CTS cancellation or user token cancellation
826+ linkedCts = CancellationTokenSource . CreateLinkedTokenSource (
827+ watchSubscriptionCts . Token ,
828+ options . Signal . Value
829+ ) ;
830+ }
831+ else
832+ {
833+ // Cancel on global CTS cancellation
834+ linkedCts = watchSubscriptionCts ;
820835 }
836+
837+ var registration = linkedCts . Token . Register ( ( ) =>
838+ {
839+ cts . Cancel ( ) ;
840+ } ) ;
841+
842+ return new WatchSubscription ( cts , registration ) ;
821843 }
822844
823845 private static void HandleTableChanges ( HashSet < string > changedTables , HashSet < string > watchedTables , Action < string [ ] > onDetectedChanges )
@@ -879,3 +901,22 @@ public class WatchOnChangeHandler
879901 public Func < WatchOnChangeEvent , Task > OnChange { get ; set ; } = null ! ;
880902 public Action < Exception > ? OnError { get ; set ; }
881903}
904+
905+ public class WatchSubscription ( CancellationTokenSource cts , CancellationTokenRegistration registration ) : IDisposable
906+ {
907+ private readonly CancellationTokenSource _cts = cts ;
908+ private readonly CancellationTokenRegistration _registration = registration ;
909+ private bool _disposed ;
910+
911+ public bool Disposed { get { return _disposed ; } }
912+
913+ public void Dispose ( )
914+ {
915+ if ( _disposed ) return ;
916+ _disposed = true ;
917+
918+ _registration . Dispose ( ) ;
919+ _cts . Cancel ( ) ;
920+ _cts . Dispose ( ) ;
921+ }
922+ }
0 commit comments