Skip to content

Commit 35cea75

Browse files
authored
Merge pull request #38 from powersync-ja/watch-disposable
Return IDisposable from Watch
2 parents 92b0a7e + 7bfc224 commit 35cea75

3 files changed

Lines changed: 217 additions & 52 deletions

File tree

PowerSync/PowerSync.Common/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# PowerSync.Common Changelog
22

33
## 0.0.8-alpha.1
4+
- Return an `IDisposable` from `PowerSync.Watch`, allowing for easier cancellation of watched queries.
45
- Replaced the old JSON-based method of extracting type information from queries with using Dapper internally for queries, improving memory usage and execution time for querying.
56
- Added non-generic overloads for `GetAll()`, `GetOptional()`, `Get()`, `Watch()` which return `dynamic`:
67

PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs

Lines changed: 85 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)