-
Notifications
You must be signed in to change notification settings - Fork 50
Incremental Watched Queries #614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 1f1b076 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
…fferentiator implementation.
Update: I've updated the implementation to guard the new watch methods behind the new Comparison based queries are now accessed via The The An automated React profiling benchmark suite has been added in |
demos/yjs-react-supabase-text-collab/supabase/functions/merge-document-updates/index.ts
Outdated
Show resolved
Hide resolved
demos/yjs-react-supabase-text-collab/src/library/powersync/PowerSyncYjsProvider.ts
Outdated
Show resolved
Hide resolved
demos/yjs-react-supabase-text-collab/src/library/powersync/PowerSyncYjsProvider.ts
Outdated
Show resolved
Hide resolved
// This ID is updated on every new instance of the provider. | ||
private id = uuidv4(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the previous approach of using the Set was simpler, since it didn't require an additional field in the database to filter the updates. We could still use the Set to only filter out the local updates, or alternatively apply those updates again (should be a no-op to apply the same update again).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed the main reason for having the Set was to avoid re-applying those updates. Given it's a no-op, I've removed this. With the differential queries we will be doing less no-ops, but we will currently still re-apply local edits.
break; | ||
updateQuery.registerListener({ | ||
onStateChange: async () => { | ||
for (const added of updateQuery.state.diff.added) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I tested this with only one tab open, I often get into a state where the initial "diff" is sent with every update, e.g. "all: 20 rows, added: 20 rows". Haven't debugged further to figure out what is going wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was due to using the logic of the onStateChange
listener. The diff
on the state
was relative to the last time the data changed, but the onStateChange
hook fires for any state change e.g. isFetching
alternating. This is a good argument for the comment below. I've removed the diff
from the state now. This query has also been updated.
|
||
export interface DifferentialWatchedQuery<RowType> | ||
extends WatchedQuery<ReadonlyArray<Readonly<RowType>>, DifferentialWatchedQuerySettings<RowType>> { | ||
readonly state: DifferentialWatchedQueryState<RowType>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't confirmed with testing, but it feels like accessing the differential state on the query could easily lead to race conditions where updates may be missed. Specifically, there could potentially be a case where the state is processed twice before it is processed in the app, leading to the diff from the first update to be missing.
With the normal async listeners this may not be an issue if the query waits for all listeners to return before processing the next one, but React hooks for example may be called at any later time.
So I'm wondering if we could send the diff in the callback, rather than the state on the query?
React hooks may have bigger issues though, since we have no control over when/how many times they are called. I'm not sure if it is actually feasible to expose the diff over a react hook's state at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW the listeners here are fired from within the onChange
callback which uses a ControlledExecutor
to ensure sequential processing.
This is a fair point though as noted in the YJS demo comment. I've removed the diff
from the state. The diff can now be accessed by registering an onDiff
listener.
Overview
Our current Watched query implementations emit results whenever a change to a dependant SQLite table occurs. The table changes might not affect the query result set, but we still query and emit a new result set for each table change. The result sets typically contain the same data, but these results are new Array/object references which will cause re-renders in certain frameworks like React.
This PR overhauls, improves and extends upon the existing watched query implementations by introducing incremental watched queries.
Incrementally Watched Queries can be constructed with varying behaviour. This PR introduces the concept of comparison and differential watched queries.
Comparison based queries behave similar to standard watched queries. These queries still query the SQLite DB under the hood on each dependant table change, but they compare the result set and only incrementally yield results if a change has been made. The latest query result is yielded as the result set.
Differential queries watch a SQL query and report detailed information on the changes between result sets. This gives additional information such as the
added
,removed
,updated
rows between result set changes.Implementation
The logic required for incrementally watched queries requires additional computation and introduces additional complexity to the implementation. For these reasons a new concept of a
WatchedQuery
class is introduced, along with a newquery
method allows building a instances ofWatchedQuery
s viawatch
anddifferentialWatch
methods.The
listsQuery
is smart, it:updateSchema
WatchedQuery
instances retain the latest state in memory. SharingWatchedQuery
instances can be used to introduce caching and reduce the number of duplicate DB queries between components.The incremental logic is customisable. Diff based queries can specify custom logic for performing diffs on the relevant data set. By default a
JSON.stringify
approach is used. Different data sets might have more optimal implementations.Updates to query parameters can be performed in a single place, affecting all subscribers.
Reactivity
The existing
watch
method and Reactivity packages have been updated to use incremental queries with differentiation defined as an opt-in feature (defaults to no changes).New hooks have also been added to use shared
WatchedQuery
instances.The Vue and React hooks packages have been updated to remove duplicate implementations of common watched query logic. Historically these packages relied on manually implementing watched queries based off the
onChange
API in order to cater for exposing additional state and custom query executors. The newWatchedQuery
APIs now support all the hook packages' requirements - this effectively reduces the heavy lifting in reactivity packages.The
React Supabase Todolist
demo has been updated with some best practices for reducing re-renders using comparison based incrementally watched queries.Differential Queries
These watched queries report the changes between result sets. A
WatchedQueryDifferential
can be accessed by registering a listener on the query.A common use case for this is processing newly created items as they are added. The
YJS React Supabase Text Collab
demo has been updated to take advantage of this feature. Document updates are watched via a differential incremental query. New updates are passed to YJS for consolidation as they are synced.Early Access
This can be tested by applying the following package versions to your project.