11namespace PowerSync . Common . Client ;
22
3- using System . Diagnostics ;
43using System . Runtime . CompilerServices ;
54using System . Text . RegularExpressions ;
5+ using System . Threading . Channels ;
66using System . Threading . Tasks ;
77
88using Microsoft . Extensions . Logging ;
99using Microsoft . Extensions . Logging . Abstractions ;
1010
1111using Newtonsoft . Json ;
12-
1312using Nito . AsyncEx ;
13+ using ThrottleDebounce ;
1414
1515using PowerSync . Common . Client . Connection ;
1616using PowerSync . Common . Client . Sync . Bucket ;
@@ -990,7 +990,7 @@ internal async Task<HashSet<string>> GetSourceTables(string sql, object?[]? para
990990 private async IAsyncEnumerable < WatchOnChangeEvent > OnRawTableChange (
991991 HashSet < string > watchedTables ,
992992 IAsyncEnumerable < DBAdapterEvents . TablesUpdatedEvent > listener ,
993- [ EnumeratorCancellation ] CancellationToken token ,
993+ [ EnumeratorCancellation ] CancellationToken signal ,
994994 bool triggerImmediately = false ,
995995 int throttleMs = DEFAULT_WATCH_THROTTLE_MS
996996 )
@@ -1014,102 +1014,66 @@ private async IAsyncEnumerable<WatchOnChangeEvent> OnRawTableChange(
10141014 yield break ;
10151015 }
10161016
1017- // Leading + trailing edge throttle
1018- var enumerator = listener . GetAsyncEnumerator ( token ) ;
1019- try
1020- {
1021- var accumulatedTables = new HashSet < string > ( ) ;
1022- var changedTables = new HashSet < string > ( ) ;
1023- long lastYieldTime = 0 ;
1024-
1025- Task < bool > moveNextTask = enumerator . MoveNextAsync ( ) . AsTask ( ) ;
1026- Task ? throttleTask = null ;
1027-
1028- while ( true )
1029- {
1030- if ( throttleTask != null )
1031- await Task . WhenAny ( moveNextTask , throttleTask ) ;
1032- else
1033- {
1034- try { await moveNextTask ; }
1035- catch ( OperationCanceledException ) { break ; }
1036- }
1017+ // Throttled - publish via throttled call to an action that flushes accumulated changes into this channel
1018+ var channel = Channel . CreateUnbounded < WatchOnChangeEvent > ( ) ;
1019+ var accumulatedTables = new HashSet < string > ( ) ;
10371020
1038- if ( throttleTask != null && throttleTask . IsCompleted && ! moveNextTask . IsCompleted )
1021+ _ = Task . Run ( async ( ) =>
1022+ {
1023+ using var throttledFlush = Throttler . Throttle ( ( ) =>
10391024 {
1040- // Throttle timer expired without a new event
1041- if ( accumulatedTables . Count > 0 )
1025+ // Safe to lock directly on accumulatedTables because it's a local variable
1026+ lock ( accumulatedTables )
10421027 {
1043- lastYieldTime = Stopwatch . GetTimestamp ( ) ;
1044- yield return new WatchOnChangeEvent { ChangedTables = [ .. accumulatedTables ] } ;
1028+ if ( accumulatedTables . Count == 0 ) return ;
1029+ channel . Writer . TryWrite ( new WatchOnChangeEvent { ChangedTables = [ .. accumulatedTables ] } ) ;
10451030 accumulatedTables . Clear ( ) ;
10461031 }
1047- throttleTask = null ;
1048- continue ;
1049- }
1050-
1051- // A new event arrived (possibly alongside throttle)
1052- // Check if the event actually exists or if this is the end of the enumerator
1053- bool hasNext ;
1054- try { hasNext = await moveNextTask ; }
1055- catch ( OperationCanceledException ) { break ; }
1056- if ( ! hasNext ) break ;
1057-
1058- // Accumulate changed tables from the most recent OnTablesUpdated event
1059- GetTablesFromNotification ( enumerator . Current . TablesUpdated , changedTables ) ;
1060-
1061- // Filter only watched tables and add to accumulatedTables set
1062- changedTables . IntersectWith ( watchedTables ) ;
1063- accumulatedTables . UnionWith ( changedTables ) ;
1032+ } ,
1033+ TimeSpan . FromMilliseconds ( throttleMs ) ,
1034+ leading : false ,
1035+ trailing : true
1036+ ) ;
10641037
1065- if ( accumulatedTables . Count > 0 )
1038+ try
1039+ {
1040+ var changedTables = new HashSet < string > ( ) ;
1041+ await foreach ( var e in listener )
10661042 {
1067- var now = Stopwatch . GetTimestamp ( ) ;
1043+ GetTablesFromNotification ( e . TablesUpdated , changedTables ) ;
1044+ changedTables . IntersectWith ( watchedTables ) ;
1045+ if ( changedTables . Count == 0 ) continue ;
10681046
1069- // There's a nice built-in method for this (Stopwatch.GetElapsedTime), but
1070- // it's not supported in .NET 6.0. :(
1071- var elapsedMs = ( now - lastYieldTime ) * 1000.0 / Stopwatch . Frequency ;
1072-
1073- if ( elapsedMs >= throttleMs )
1047+ lock ( accumulatedTables ) { accumulatedTables . UnionWith ( changedTables ) ; }
1048+ throttledFlush . Invoke ( ) ;
1049+ }
1050+ }
1051+ catch ( OperationCanceledException ) { }
1052+ finally
1053+ {
1054+ // Flush any remaining events and close the channel
1055+ lock ( accumulatedTables )
1056+ {
1057+ if ( accumulatedTables . Count > 0 )
10741058 {
1075- // First event since throttle expiration
1076- // Fire immediately (leading edge) and reset throttle timer
1077- lastYieldTime = now ;
1078- yield return new WatchOnChangeEvent { ChangedTables = [ .. accumulatedTables ] } ;
1059+ channel . Writer . TryWrite ( new WatchOnChangeEvent { ChangedTables = [ .. accumulatedTables ] } ) ;
10791060 accumulatedTables . Clear ( ) ;
1080- throttleTask = null ;
1081- }
1082- else
1083- {
1084- throttleTask ??= Task . Delay ( ( int ) ( throttleMs - elapsedMs ) , token ) ;
10851061 }
10861062 }
1087-
1088- moveNextTask = enumerator . MoveNextAsync ( ) . AsTask ( ) ;
1063+ channel . Writer . Complete ( ) ;
10891064 }
1065+ } ) ;
10901066
1091- // Flush any remaining events
1092- if ( accumulatedTables . Count > 0 )
1093- yield return new WatchOnChangeEvent { ChangedTables = [ .. accumulatedTables ] } ;
1094- }
1095- finally
1067+ // Continuously pull values from channel and publish to the consumer
1068+ while ( await channel . Reader . WaitToReadAsync ( CancellationToken . None ) )
10961069 {
1097- await enumerator . DisposeAsync ( ) ;
1070+ while ( channel . Reader . TryRead ( out var evt ) )
1071+ {
1072+ yield return evt ;
1073+ }
10981074 }
10991075 }
11001076
1101- private static void AccumulateMatchingTables (
1102- DBAdapterEvents . TablesUpdatedEvent e ,
1103- HashSet < string > watchedTables ,
1104- HashSet < string > accumulated
1105- )
1106- {
1107- var tables = new HashSet < string > ( ) ;
1108- GetTablesFromNotification ( e . TablesUpdated , tables ) ;
1109- tables . IntersectWith ( watchedTables ) ;
1110- accumulated . UnionWith ( tables ) ;
1111- }
1112-
11131077 private static void GetTablesFromNotification ( INotification updateNotification , HashSet < string > changedTables )
11141078 {
11151079 changedTables . Clear ( ) ;
0 commit comments