Skip to content

Commit 2d22a29

Browse files
authored
Merge pull request #555 from powersync-ja/sync-progress
Sync progress
2 parents 7784955 + 1de9e65 commit 2d22a29

File tree

26 files changed

+817
-99
lines changed

26 files changed

+817
-99
lines changed

.changeset/sharp-singers-search.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@powersync/common': minor
3+
'@powersync/web': minor
4+
'@powersync/node': minor
5+
'@powersync/react-native': minor
6+
---
7+
8+
Report progress information about downloaded rows. Sync progress is available through `SyncStatus.downloadProgress`.

demos/django-react-native-todolist/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"@azure/core-asynciterator-polyfill": "^1.0.2",
1212
"@expo/metro-runtime": "^4.0.1",
1313
"@expo/vector-icons": "^14.0.0",
14-
"@journeyapps/react-native-quick-sqlite": "^2.4.3",
14+
"@journeyapps/react-native-quick-sqlite": "^2.4.4",
1515
"@powersync/common": "workspace:*",
1616
"@powersync/react": "workspace:*",
1717
"@powersync/react-native": "workspace:*",

demos/react-native-supabase-group-chat/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@azure/core-asynciterator-polyfill": "^1.0.2",
2323
"@expo/metro-runtime": "^4.0.1",
2424
"@faker-js/faker": "8.3.1",
25-
"@journeyapps/react-native-quick-sqlite": "^2.4.3",
25+
"@journeyapps/react-native-quick-sqlite": "^2.4.4",
2626
"@powersync/common": "workspace:*",
2727
"@powersync/react": "workspace:*",
2828
"@powersync/react-native": "workspace:*",

demos/react-native-supabase-todolist/app/views/todos/lists.tsx

+22-22
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ import prompt from 'react-native-prompt-android';
77
import { router, Stack } from 'expo-router';
88
import { LIST_TABLE, TODO_TABLE, ListRecord } from '../../../library/powersync/AppSchema';
99
import { useSystem } from '../../../library/powersync/system';
10-
import { useQuery, useStatus } from '@powersync/react-native';
10+
import { useQuery } from '@powersync/react-native';
1111
import { ListItemWidget } from '../../../library/widgets/ListItemWidget';
12+
import { GuardBySync } from '../../../library/widgets/GuardBySync';
1213

1314
const description = (total: number, completed: number = 0) => {
1415
return `${total - completed} pending, ${completed} completed`;
1516
};
1617

1718
const ListsViewWidget: React.FC = () => {
1819
const system = useSystem();
19-
const status = useStatus();
2020
const { data: listRecords } = useQuery<ListRecord & { total_tasks: number; completed_tasks: number }>(`
2121
SELECT
2222
${LIST_TABLE}.*, COUNT(${TODO_TABLE}.id) AS total_tasks, SUM(CASE WHEN ${TODO_TABLE}.completed = true THEN 1 ELSE 0 END) as completed_tasks
@@ -78,26 +78,26 @@ const ListsViewWidget: React.FC = () => {
7878
);
7979
}}
8080
/>
81-
<ScrollView key={'lists'} style={{ maxHeight: '90%' }}>
82-
{!status.hasSynced ? (
83-
<Text>Busy with sync...</Text>
84-
) : (
85-
listRecords.map((r) => (
86-
<ListItemWidget
87-
key={r.id}
88-
title={r.name}
89-
description={description(r.total_tasks, r.completed_tasks)}
90-
onDelete={() => deleteList(r.id)}
91-
onPress={() => {
92-
router.push({
93-
pathname: 'views/todos/edit/[id]',
94-
params: { id: r.id }
95-
});
96-
}}
97-
/>
98-
))
99-
)}
100-
</ScrollView>
81+
<GuardBySync>
82+
<ScrollView key={'lists'} style={{ maxHeight: '90%' }}>
83+
{(
84+
listRecords.map((r) => (
85+
<ListItemWidget
86+
key={r.id}
87+
title={r.name!}
88+
description={description(r.total_tasks, r.completed_tasks)}
89+
onDelete={() => deleteList(r.id)}
90+
onPress={() => {
91+
router.push({
92+
pathname: 'views/todos/edit/[id]',
93+
params: { id: r.id }
94+
});
95+
}}
96+
/>
97+
))
98+
)}
99+
</ScrollView>
100+
</GuardBySync>
101101

102102
<StatusBar style={'light'} />
103103
</View>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useStatus } from '@powersync/react';
2+
import { FC, ReactNode } from 'react';
3+
import { View } from 'react-native';
4+
import { Text, LinearProgress } from '@rneui/themed';
5+
6+
/**
7+
* A component that renders its child if the database has been synced at least once and shows
8+
* a progress indicator otherwise.
9+
*/
10+
export const GuardBySync: FC<{ children: ReactNode; priority?: number }> = ({ children, priority }) => {
11+
const status = useStatus();
12+
13+
const hasSynced = priority == null ? status.hasSynced : status.statusForPriority(priority).hasSynced;
14+
if (hasSynced) {
15+
return children;
16+
}
17+
18+
// If we haven't completed a sync yet, show a progress indicator!
19+
const allProgress = status.downloadProgress;
20+
const progress = priority == null ? allProgress : allProgress?.untilPriority(priority);
21+
22+
return (
23+
<View>
24+
{progress != null ? (
25+
<>
26+
<LinearProgress variant="determinate" value={progress.downloadedFraction} />
27+
{progress.downloadedOperations == progress.totalOperations ? (
28+
<Text>Applying server-side changes</Text>
29+
) : (
30+
<Text>
31+
Downloaded {progress.downloadedOperations} out of {progress.totalOperations}.
32+
</Text>
33+
)}
34+
</>
35+
) : (
36+
<LinearProgress variant="indeterminate" />
37+
)}
38+
</View>
39+
);
40+
};

demos/react-native-supabase-todolist/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"dependencies": {
1111
"@azure/core-asynciterator-polyfill": "^1.0.2",
1212
"@expo/vector-icons": "^14.0.3",
13-
"@journeyapps/react-native-quick-sqlite": "^2.4.3",
13+
"@journeyapps/react-native-quick-sqlite": "^2.4.4",
1414
"@powersync/attachments": "workspace:*",
1515
"@powersync/common": "workspace:*",
1616
"@powersync/react": "workspace:*",

demos/react-supabase-todolist/src/app/views/todo-lists/page.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { LISTS_TABLE } from '@/library/powersync/AppSchema';
1818
import { NavigationPage } from '@/components/navigation/NavigationPage';
1919
import { SearchBarWidget } from '@/components/widgets/SearchBarWidget';
2020
import { TodoListsWidget } from '@/components/widgets/TodoListsWidget';
21+
import { GuardBySync } from '@/components/widgets/GuardBySync';
2122

2223
export default function TodoListsPage() {
2324
const powerSync = usePowerSync();
@@ -53,7 +54,9 @@ export default function TodoListsPage() {
5354
</S.FloatingActionButton>
5455
<Box>
5556
<SearchBarWidget />
56-
{!status.hasSynced ? <p>Busy with sync...</p> : <TodoListsWidget />}
57+
<GuardBySync>
58+
<TodoListsWidget />
59+
</GuardBySync>
5760
</Box>
5861
{/* TODO use a dialog service in future, this is just a simple example app */}
5962
<Dialog
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Box, LinearProgress, Stack, Typography } from '@mui/material';
2+
import { useStatus } from '@powersync/react';
3+
import { FC, ReactNode } from 'react';
4+
5+
/**
6+
* A component that renders its child if the database has been synced at least once and shows
7+
* a progress indicator otherwise.
8+
*/
9+
export const GuardBySync: FC<{ children: ReactNode; priority?: number }> = ({ children, priority }) => {
10+
const status = useStatus();
11+
12+
const hasSynced = priority == null ? status.hasSynced : status.statusForPriority(priority).hasSynced;
13+
if (hasSynced) {
14+
return children;
15+
}
16+
17+
// If we haven't completed a sync yet, show a progress indicator!
18+
const allProgress = status.downloadProgress;
19+
const progress = priority == null ? allProgress : allProgress?.untilPriority(priority);
20+
21+
return (
22+
<Stack direction="column" spacing={1} sx={{ p: 4 }} alignItems="stretch">
23+
{progress != null ? (
24+
<>
25+
<LinearProgress variant="determinate" value={progress.downloadedFraction * 100} />
26+
<Box sx={{ alignSelf: 'center' }}>
27+
{progress.downloadedOperations == progress.totalOperations ? (
28+
<Typography>Applying server-side changes</Typography>
29+
) : (
30+
<Typography>
31+
Downloaded {progress.downloadedOperations} out of {progress.totalOperations}.
32+
</Typography>
33+
)}
34+
</Box>
35+
</>
36+
) : (
37+
<LinearProgress variant="indeterminate" />
38+
)}
39+
</Stack>
40+
);
41+
};

packages/common/src/client/AbstractPowerSyncDatabase.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
type PowerSyncConnectionOptions,
3333
type RequiredAdditionalConnectionOptions
3434
} from './sync/stream/AbstractStreamingSyncImplementation.js';
35+
import { FULL_SYNC_PRIORITY } from '../db/crud/SyncProgress.js';
3536

3637
export interface DisconnectAndClearOptions {
3738
/** When set to false, data in local-only tables is preserved. */
@@ -146,11 +147,6 @@ export const isPowerSyncDatabaseOptionsWithSettings = (test: any): test is Power
146147
return typeof test == 'object' && isSQLOpenOptions(test.database);
147148
};
148149

149-
/**
150-
* The priority used by the core extension to indicate that a full sync was completed.
151-
*/
152-
const FULL_SYNC_PRIORITY = 2147483647;
153-
154150
export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDBListener> {
155151
/**
156152
* Transactions should be queued in the DBAdapter, but we also want to prevent

packages/common/src/client/SQLOpenFactory.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export interface SQLOpenOptions {
77
dbFilename: string;
88
/**
99
* Directory where the database file is located.
10-
*
10+
*
1111
* When set, the directory must exist when the database is opened, it will
1212
* not be created automatically.
1313
*/

packages/common/src/client/sync/bucket/BucketStorageAdapter.ts

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ export interface SyncLocalDatabaseResult {
3030
checkpointFailures?: string[];
3131
}
3232

33+
export type SavedProgress = {
34+
atLast: number;
35+
sinceLast: number;
36+
};
37+
38+
export type BucketOperationProgress = Record<string, SavedProgress>;
39+
3340
export interface BucketChecksum {
3441
bucket: string;
3542
priority?: number;
@@ -65,6 +72,7 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
6572
startSession(): void;
6673

6774
getBucketStates(): Promise<BucketState[]>;
75+
getBucketOperationProgress(): Promise<BucketOperationProgress>;
6876

6977
syncLocalDatabase(
7078
checkpoint: Checkpoint,

packages/common/src/client/sync/bucket/SqliteBucketStorage.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BaseObserver } from '../../../utils/BaseObserver.js';
55
import { MAX_OP_ID } from '../../constants.js';
66
import {
77
BucketChecksum,
8+
BucketOperationProgress,
89
BucketState,
910
BucketStorageAdapter,
1011
BucketStorageListener,
@@ -91,6 +92,13 @@ export class SqliteBucketStorage extends BaseObserver<BucketStorageListener> imp
9192
return result;
9293
}
9394

95+
async getBucketOperationProgress(): Promise<BucketOperationProgress> {
96+
const rows = await this.db.getAll<{ name: string; count_at_last: number; count_since_last: number }>(
97+
'SELECT name, count_at_last, count_since_last FROM ps_buckets'
98+
);
99+
return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
100+
}
101+
94102
async saveSyncData(batch: SyncDataBatch) {
95103
await this.writeTransaction(async (tx) => {
96104
let count = 0;
@@ -199,7 +207,21 @@ export class SqliteBucketStorage extends BaseObserver<BucketStorageListener> imp
199207
'sync_local',
200208
arg
201209
]);
202-
return result == 1;
210+
if (result == 1) {
211+
if (priority == null) {
212+
const bucketToCount = Object.fromEntries(checkpoint.buckets.map((b) => [b.bucket, b.count]));
213+
// The two parameters could be replaced with one, but: https://github.com/powersync-ja/better-sqlite3/pull/6
214+
const jsonBucketCount = JSON.stringify(bucketToCount);
215+
await tx.execute(
216+
"UPDATE ps_buckets SET count_since_last = 0, count_at_last = ?->name WHERE name != '$local' AND ?->name IS NOT NULL",
217+
[jsonBucketCount, jsonBucketCount]
218+
);
219+
}
220+
221+
return true;
222+
} else {
223+
return false;
224+
}
203225
});
204226
}
205227

0 commit comments

Comments
 (0)