Skip to content

[WIP] 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

Draft
wants to merge 49 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8eb570d
wip
stevensJourney May 13, 2025
861fc2b
wip
stevensJourney May 15, 2025
f6b3ef4
Improve hook implementation
stevensJourney May 16, 2025
6a18a4a
update tests to use web db
stevensJourney May 16, 2025
e6f902e
Add comparison test
stevensJourney May 16, 2025
ddbf6a3
cleanup
stevensJourney May 19, 2025
9837ca9
limit stream depth
stevensJourney May 19, 2025
7b15fc3
use abort signal
stevensJourney May 19, 2025
8874c75
cleanup api
stevensJourney May 20, 2025
7d1124c
Update generic typing and APIs
stevensJourney May 20, 2025
69f7394
wip: react suspense
stevensJourney May 21, 2025
83e99e2
wip: query interface
stevensJourney May 22, 2025
4704723
cleanup interfaces
stevensJourney May 26, 2025
79f809e
cleanup suspense hooks
stevensJourney May 27, 2025
b11c24e
wip: split suspense hooks
stevensJourney May 27, 2025
9b86a63
git mv
stevensJourney May 27, 2025
e98e43f
cleanup hook folder structure
stevensJourney May 27, 2025
fccbd69
more hook cleanup
stevensJourney May 27, 2025
0ce4ddf
git mv
stevensJourney May 27, 2025
3009ac7
cleanup files
stevensJourney May 27, 2025
151dc0a
temp
stevensJourney May 27, 2025
0b4926e
rename
stevensJourney May 27, 2025
e4a8a2d
mv
stevensJourney May 27, 2025
f273312
more react cleanup
stevensJourney May 27, 2025
83b1289
update unit tests
stevensJourney May 27, 2025
719d396
Merge remote-tracking branch 'origin/main' into watches
stevensJourney May 27, 2025
4c5b136
Add tests for shared queries
stevensJourney May 27, 2025
4581a97
Merge remote-tracking branch 'origin/main' into watches
stevensJourney Jun 2, 2025
40b849c
cleanup compatible queries
stevensJourney Jun 2, 2025
81d68e3
maintain backwards compatibility
stevensJourney Jun 2, 2025
65a0ea3
Simplify query options for watched queries
stevensJourney Jun 2, 2025
5ba28b4
update React README
stevensJourney Jun 2, 2025
9832417
cleanup React demo
stevensJourney Jun 2, 2025
6036652
update vue unit tests to use an actual DB
stevensJourney Jun 3, 2025
c930e0e
fix unit tests. Improve closing behaviour.
stevensJourney Jun 3, 2025
328aaa8
fix React tests
stevensJourney Jun 3, 2025
642c11a
remove log
stevensJourney Jun 3, 2025
40587bb
Merge remote-tracking branch 'origin/main' into watches
stevensJourney Jun 3, 2025
4f54dab
cleanup
stevensJourney Jun 3, 2025
3a6520e
use enums
stevensJourney Jun 3, 2025
aab75f3
cleanup
stevensJourney Jun 3, 2025
c3e4709
use a builder pattern to separate incremental watched query types.
stevensJourney Jun 3, 2025
c108094
fix web tests
stevensJourney Jun 3, 2025
a0e0c70
improve generics
stevensJourney Jun 3, 2025
05f50f1
Add demo for instant query caching with localStorage
stevensJourney Jun 3, 2025
0d01b97
revert headless setting
stevensJourney Jun 3, 2025
12b6aa8
fix kysely test
stevensJourney Jun 4, 2025
8208ac0
cleanup example JSDoc comments
stevensJourney Jun 4, 2025
8a42cda
cleanup
stevensJourney Jun 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-bananas-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/common': minor
---

Added additional listeners for `closing` and `closed` events in `AbstractPowerSyncDatabase`.
5 changes: 5 additions & 0 deletions .changeset/stale-dots-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/web': minor
---

Improved query behaviour when client is closed. Pending requests will be aborted, future requests will be rejected with an Error. Fixed read and write lock requests not respecting timeout parameter.
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"jsxBracketSameLine": true,
"useTabs": false,
"printWidth": 120,
"trailingComma": "none"
"trailingComma": "none",
"plugins": ["prettier-plugin-embed", "prettier-plugin-sql"]
}
90 changes: 54 additions & 36 deletions demos/react-supabase-todolist/src/app/views/sql-console/page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,73 @@
import React from 'react';
import { useQuery } from '@powersync/react';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { Box, Button, Grid, TextField, styled } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { useQuery } from '@powersync/react';
import { ArrayComparator } from '@powersync/web';
import React from 'react';

export type LoginFormParams = {
email: string;
password: string;
};

const DEFAULT_QUERY = 'SELECT * FROM lists';

export default function SQLConsolePage() {
const inputRef = React.useRef<HTMLInputElement>();
const [query, setQuery] = React.useState(DEFAULT_QUERY);
const { data: querySQLResult } = useQuery(query);
const DEFAULT_QUERY = /* sql */ `
SELECT
*
FROM
lists
`;

const TableDisplay = React.memo(({ data }: { data: any[] }) => {
const queryDataGridResult = React.useMemo(() => {
const firstItem = querySQLResult?.[0];

const firstItem = data?.[0];
return {
columns: firstItem
? Object.keys(firstItem).map((field) => ({
field,
flex: 1
}))
: [],
rows: querySQLResult
rows: data
};
}, [querySQLResult]);
}, [data]);

return (
<S.QueryResultContainer>
<DataGrid
autoHeight={true}
rows={queryDataGridResult.rows.map((r, index) => ({ ...r, id: r.id ?? index })) ?? []}
columns={queryDataGridResult.columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 20
}
}
}}
pageSizeOptions={[20]}
disableRowSelectionOnClick
/>
</S.QueryResultContainer>
);
});

export default function SQLConsolePage() {
const inputRef = React.useRef<HTMLInputElement>();
const [query, setQuery] = React.useState(DEFAULT_QUERY);

const { data } = useQuery(query, [], {
/**
* We don't use the isFetching status here, we can avoid re-renders if we don't report on it.
*/
reportFetching: false,
/**
* The query here will only emit results when the query data set changes.
* Result sets are compared by serializing each item to JSON and comparing the strings.
*/
comparator: new ArrayComparator({
compareBy: (item) => JSON.stringify(item)
})
});

return (
<NavigationPage title="SQL Console">
Expand Down Expand Up @@ -57,33 +96,12 @@ export default function SQLConsolePage() {
if (queryInput) {
setQuery(queryInput);
}
}}
>
}}>
Execute Query
</Button>
</S.CenteredGrid>
</S.CenteredGrid>

{queryDataGridResult ? (
<S.QueryResultContainer>
{queryDataGridResult.columns ? (
<DataGrid
autoHeight={true}
rows={queryDataGridResult.rows?.map((r, index) => ({ ...r, id: r.id ?? index })) ?? []}
columns={queryDataGridResult.columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 20
}
}
}}
pageSizeOptions={[20]}
disableRowSelectionOnClick
/>
) : null}
</S.QueryResultContainer>
) : null}
<TableDisplay data={data} />
</S.MainContainer>
</NavigationPage>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { usePowerSync, useQuery } from '@powersync/react';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { useSupabase } from '@/components/providers/SystemProvider';
import { TodoItemWidget } from '@/components/widgets/TodoItemWidget';
import { LISTS_TABLE, TODOS_TABLE, TodoRecord } from '@/library/powersync/AppSchema';
import AddIcon from '@mui/icons-material/Add';
import {
Box,
Expand All @@ -15,12 +18,9 @@ import {
styled
} from '@mui/material';
import Fab from '@mui/material/Fab';
import { usePowerSync, useQuery } from '@powersync/react';
import React, { Suspense } from 'react';
import { useParams } from 'react-router-dom';
import { useSupabase } from '@/components/providers/SystemProvider';
import { LISTS_TABLE, TODOS_TABLE, TodoRecord } from '@/library/powersync/AppSchema';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { TodoItemWidget } from '@/components/widgets/TodoItemWidget';

/**
* useSearchParams causes the entire element to fall back to client side rendering
Expand All @@ -34,61 +34,53 @@ const TodoEditSection = () => {

const {
data: [listRecord]
} = useQuery<{ name: string }>(`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`, [listID]);
} = useQuery<{ name: string }>(
/* sql */ `
SELECT
name
FROM
${LISTS_TABLE}
WHERE
id = ?
`,
[listID]
);

const { data: todos } = useQuery<TodoRecord>(
`SELECT * FROM ${TODOS_TABLE} WHERE list_id=? ORDER BY created_at DESC, id`,
/* sql */ `
SELECT
*
FROM
${TODOS_TABLE}
WHERE
list_id = ?
ORDER BY
created_at DESC,
id
`,
[listID]
);

const [showPrompt, setShowPrompt] = React.useState(false);
const nameInputRef = React.createRef<HTMLInputElement>();

const toggleCompletion = async (record: TodoRecord, completed: boolean) => {
const updatedRecord = { ...record, completed: completed };
if (completed) {
const userID = supabase?.currentSession?.user.id;
if (!userID) {
throw new Error(`Could not get user ID.`);
}
updatedRecord.completed_at = new Date().toISOString();
updatedRecord.completed_by = userID;
} else {
updatedRecord.completed_at = null;
updatedRecord.completed_by = null;
}
await powerSync.execute(
`UPDATE ${TODOS_TABLE}
SET completed = ?,
completed_at = ?,
completed_by = ?
WHERE id = ?`,
[completed, updatedRecord.completed_at, updatedRecord.completed_by, record.id]
);
};

const createNewTodo = async (description: string) => {
const userID = supabase?.currentSession?.user.id;
if (!userID) {
throw new Error(`Could not get user ID.`);
}

await powerSync.execute(
`INSERT INTO
${TODOS_TABLE}
(id, created_at, created_by, description, list_id)
VALUES
(uuid(), datetime(), ?, ?, ?)`,
/* sql */ `
INSERT INTO
${TODOS_TABLE} (id, created_at, created_by, description, list_id)
VALUES
(uuid (), datetime (), ?, ?, ?)
`,
[userID, description, listID!]
);
};

const deleteTodo = async (id: string) => {
await powerSync.writeTransaction(async (tx) => {
await tx.execute(`DELETE FROM ${TODOS_TABLE} WHERE id = ?`, [id]);
});
};

if (!listRecord) {
return (
<Box>
Expand All @@ -106,13 +98,7 @@ const TodoEditSection = () => {
<Box>
<List dense={false}>
{todos.map((r) => (
<TodoItemWidget
key={r.id}
description={r.description}
onDelete={() => deleteTodo(r.id)}
isComplete={r.completed == 1}
toggleCompletion={() => toggleCompletion(r, !r.completed)}
/>
<TodoItemWidget key={r.id} id={r.id} description={r.description} isComplete={r.completed == 1} />
))}
</List>
</Box>
Expand All @@ -129,8 +115,7 @@ const TodoEditSection = () => {
await createNewTodo(nameInputRef.current!.value);
setShowPrompt(false);
}
}}
>
}}>
<DialogTitle id="alert-dialog-title">{'Create Todo Item'}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">Enter a description for a new todo item</DialogContentText>
Expand Down
25 changes: 14 additions & 11 deletions demos/react-supabase-todolist/src/app/views/todo-lists/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { usePowerSync, useStatus } from '@powersync/react';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { useSupabase } from '@/components/providers/SystemProvider';
import { GuardBySync } from '@/components/widgets/GuardBySync';
import { SearchBarWidget } from '@/components/widgets/SearchBarWidget';
import { TodoListsWidget } from '@/components/widgets/TodoListsWidget';
import { LISTS_TABLE } from '@/library/powersync/AppSchema';
import AddIcon from '@mui/icons-material/Add';
import {
Box,
Expand All @@ -12,18 +17,12 @@ import {
styled
} from '@mui/material';
import Fab from '@mui/material/Fab';
import { usePowerSync } from '@powersync/react';
import React from 'react';
import { useSupabase } from '@/components/providers/SystemProvider';
import { LISTS_TABLE } from '@/library/powersync/AppSchema';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { SearchBarWidget } from '@/components/widgets/SearchBarWidget';
import { TodoListsWidget } from '@/components/widgets/TodoListsWidget';
import { GuardBySync } from '@/components/widgets/GuardBySync';

export default function TodoListsPage() {
const powerSync = usePowerSync();
const supabase = useSupabase();
const status = useStatus();

const [showPrompt, setShowPrompt] = React.useState(false);
const nameInputRef = React.createRef<HTMLInputElement>();
Expand All @@ -36,7 +35,12 @@ export default function TodoListsPage() {
}

const res = await powerSync.execute(
`INSERT INTO ${LISTS_TABLE} (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?) RETURNING *`,
/* sql */ `
INSERT INTO
${LISTS_TABLE} (id, created_at, name, owner_id)
VALUES
(uuid (), datetime (), ?, ?) RETURNING *
`,
[name, userID]
);

Expand Down Expand Up @@ -71,8 +75,7 @@ export default function TodoListsPage() {
}
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
aria-describedby="alert-dialog-description">
<DialogTitle id="alert-dialog-title">{'Create Todo List'}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">Enter a name for a new todo list</DialogContentText>
Expand Down
Loading