Skip to content

feat!: return IAsyncEnumerable from Watch and OnChange#43

Merged
LucDeCaf merged 8 commits intomainfrom
watch-async-enumerable
Feb 25, 2026
Merged

feat!: return IAsyncEnumerable from Watch and OnChange#43
LucDeCaf merged 8 commits intomainfrom
watch-async-enumerable

Conversation

@LucDeCaf
Copy link
Copy Markdown
Contributor

@LucDeCaf LucDeCaf commented Feb 18, 2026

Status

TODOS

  • Implementation
  • Use within codebase / demos
  • Replace Listen with ListenAsync for most/all uses
  • Better tests / testing practices

Description

Most SDKs have moved away from the callback-based APIs and towards async iterators/enumerators. This comes with a few ergonomic benefits and a few practical ones, including the ability to subscribe to changes lazily. Using an async iterator means users can decide when they want to process the next item in the stream, whereas with callbacks, changes are pushed eagerly and are processed when we are ready instead of when the caller is ready.

I want to avoid maintaining two APIs for this, as doing so could lead to a situation like the one in the Javascript SDK, where both approaches need to be maintained in order to maintain API stability guarantees. This makes the implementation code for watched queries in JS quite difficult to follow. Plus, the eager-fetching behaviour from the old callback style can still be achieved via a background task, so the callback syntax is somewhat obsoleted by the async iterator syntax.

This PR replaces the callback-based maintenance nightmare that was the old Watch and OnChange implementations and return types with methods that return IAsyncEnumerable's, using the Kotlin SDK as a reference. IMO, this makes Watch both more flexible and easier to maintain.

Example

Old syntax

// Optional cancellation token
var cts = new CancellationTokenSource();

// Register listener synchronously on main thread (callbacks run on separate thread)
var dispose = db.Watch<Todo>(
    "SELECT * FROM todos",
    [],
    new WatchHandler()
    {
        OnResult = (result) =>
        {
            Console.WriteLine($"Number of todos: {result.Length}");
        },
        OnError = (ex) => { throw ex; }
    },
    new SQLWatchOptions { Signal = cts.Token }
);

// Stop watching via IDisposable callback or by cancelling token
dispose.Dispose();
// OR:
// cts.Cancel();

New syntax

// Register listener synchronously on main thread
var listener = db.Watch<Todo>(
    "SELECT * FROM todos",
    [],
    new SQLWatchOptions { Signal = cts.Token }
);

// ...then listen to changes on another thread
_ = Task.Run(async () =>
{
    await foreach (var result in listener)
    {
        Console.WriteLine($"Number of todos: {result.Length}");
    }
}, cts.Token);

// Stop watching by cancelling token
cts.Cancel();

@LucDeCaf LucDeCaf changed the title [POC] Return IAsyncEnumerable from Watch and OnChange feat!: return IAsyncEnumerable from Watch and OnChange Feb 19, 2026
@LucDeCaf LucDeCaf marked this pull request as ready for review February 23, 2026 07:52
Copy link
Copy Markdown
Collaborator

@Chriztiaan Chriztiaan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy with the changes from my side.

Chriztiaan
Chriztiaan previously approved these changes Feb 23, 2026
@Chriztiaan
Copy link
Copy Markdown
Collaborator

Changelog entry

@LucDeCaf LucDeCaf merged commit 77d33d4 into main Feb 25, 2026
1 check passed
@LucDeCaf LucDeCaf deleted the watch-async-enumerable branch February 25, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants