Skip to content

Commit 560d783

Browse files
committed
Added useSyncStreams reproduction
1 parent 44f1c4d commit 560d783

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

demos/react-supabase-time-based-sync/src/app/views/issues/page.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { NavigationPage } from '@/components/navigation/NavigationPage';
22
import { IssueItemWidget } from '@/components/widgets/IssueItemWidget';
33
import { ISSUES_TABLE, IssueRecord } from '@/library/powersync/AppSchema';
4+
import { useSyncStreams } from '@/library/hooks/useSyncStreams';
45
import { Box, Chip, List, Stack, Typography, styled } from '@mui/material';
56
import { useQuery } from '@powersync/react';
67
import React from 'react';
78

9+
// const AVAILABLE_DATES = ['2026-01-15'];
810
const AVAILABLE_DATES = ['2026-01-15', '2026-01-14', '2026-01-10', '2026-01-07', '2026-01-05'];
911

1012
export default function IssuesPage() {
@@ -18,12 +20,22 @@ export default function IssuesPage() {
1820

1921
const streams = selectedDates.map((date) => ({ name: 'issues_by_date', parameters: { date }, ttl: 0 }));
2022

23+
// --- Option A: useQuery with built-in streams support ---
24+
// useQuery manages subscriptions internally. Comment out Option B when using this.
2125
const { data: issues } = useQuery<IssueRecord>(
2226
`SELECT * FROM ${ISSUES_TABLE} ORDER BY updated_at DESC`,
2327
[],
2428
{ streams }
2529
);
2630

31+
// --- Option B: useSyncStreams hook (custom) + plain useQuery ---
32+
// useSyncStreams manages subscriptions separately from the query.
33+
// Comment out Option A and uncomment both lines below when using this.
34+
// useSyncStreams(streams);
35+
// const { data: issues } = useQuery<IssueRecord>(`SELECT * FROM ${ISSUES_TABLE} ORDER BY updated_at DESC`);
36+
37+
console.log('streams', streams.length);
38+
2739
return (
2840
<NavigationPage title="Issues (Time-Based Sync)">
2941
<Box>

demos/react-supabase-time-based-sync/src/components/providers/SystemProvider.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { AppSchema } from '@/library/powersync/AppSchema';
22
import { SupabaseConnector } from '@/library/powersync/SupabaseConnector';
33
import { CircularProgress } from '@mui/material';
44
import { PowerSyncContext } from '@powersync/react';
5-
import { createBaseLogger, LogLevel, PowerSyncDatabase, SyncClientImplementation } from '@powersync/web';
5+
import { createBaseLogger, LogLevel, PowerSyncDatabase, SyncClientImplementation, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web';
66
import React, { Suspense } from 'react';
77
import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext';
88

@@ -11,9 +11,10 @@ export const useSupabase = () => React.useContext(SupabaseContext);
1111

1212
export const db = new PowerSyncDatabase({
1313
schema: AppSchema,
14-
database: {
15-
dbFilename: 'time.db'
16-
}
14+
database: new WASQLiteOpenFactory({
15+
dbFilename: 'time-sync.db',
16+
vfs: WASQLiteVFS.OPFSCoopSyncVFS
17+
})
1718
});
1819

1920
export const SystemProvider = ({ children }: { children: React.ReactNode }) => {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useEffect, useMemo } from 'react';
2+
import { usePowerSync, useStatus, UseSyncStreamOptions } from '@powersync/react';
3+
4+
/**
5+
* Creates multiple PowerSync stream subscriptions. Subscriptions are kept alive as long as the
6+
* React component calling this function. When it unmounts, or when the streams array contents
7+
* change, all previous subscriptions are unsubscribed before new ones are created.
8+
*/
9+
export function useSyncStreams(streams: UseSyncStreamOptions[]) {
10+
const db = usePowerSync();
11+
const status = useStatus();
12+
13+
// Serialize streams so the effect only re-runs when content actually changes.
14+
// We also parse it back so the effect closure uses the EXACT same streams that triggered it —
15+
// avoiding the stale-ref problem where streamsRef.current may have advanced to a newer render
16+
// by the time the effect flushes.
17+
const serialized = useMemo(() => JSON.stringify(streams), [streams]);
18+
const frozenStreams = useMemo<UseSyncStreamOptions[]>(() => JSON.parse(serialized), [serialized]);
19+
20+
useEffect(() => {
21+
const abort = new AbortController();
22+
23+
const promises = frozenStreams.map((options) =>
24+
db.syncStream(options.name, options.parameters ?? undefined).subscribe(options)
25+
);
26+
27+
Promise.all(promises).then((resolvedSubs) => {
28+
if (abort.signal.aborted) {
29+
// Cleanup already ran before all promises resolved — unsubscribe immediately.
30+
for (const sub of resolvedSubs) {
31+
sub.unsubscribe();
32+
}
33+
return;
34+
}
35+
36+
// Cleanup will run eventually — unsubscribe when it does.
37+
abort.signal.addEventListener('abort', () => {
38+
for (const sub of resolvedSubs) {
39+
sub.unsubscribe();
40+
}
41+
});
42+
});
43+
44+
return () => abort.abort();
45+
}, [frozenStreams]);
46+
47+
return useMemo(
48+
() =>
49+
streams.map((options) =>
50+
status.forStream({ name: options.name, parameters: options.parameters ?? null }) ?? null
51+
),
52+
// eslint-disable-next-line react-hooks/exhaustive-deps
53+
[status, serialized]
54+
);
55+
}

0 commit comments

Comments
 (0)