Skip to content

Commit a7e4287

Browse files
committed
Updated useSyncStreams to use cleanedUp boolean instead of AbortController
1 parent 8de6d73 commit a7e4287

2 files changed

Lines changed: 27 additions & 27 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function IssuesPage() {
1818
);
1919
};
2020

21-
const streams = selectedDates.map((date) => ({ name: 'issues_by_date', parameters: { date }, ttl: 5 }));
21+
const streams = selectedDates.map((date) => ({ name: 'issues_by_date', parameters: { date }, ttl: 0 }));
2222

2323
// --- Option A: useQuery with built-in streams support ---
2424
// useQuery manages subscriptions internally.

demos/react-supabase-time-based-sync/src/library/hooks/useSyncStreams.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,39 @@ export function useSyncStreams(streams: UseSyncStreamOptions[]) {
1010
const db = usePowerSync();
1111
const status = useStatus();
1212

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.
13+
// Serialize to a string so the effect dep is a stable primitive.
14+
// Parsed back inside the effect so the closure always uses the exact snapshot for this run.
1715
const serialized = useMemo(() => JSON.stringify(streams), [streams]);
18-
const frozenStreams = useMemo<UseSyncStreamOptions[]>(() => JSON.parse(serialized), [serialized]);
1916

2017
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) {
18+
const currentStreams: UseSyncStreamOptions[] = JSON.parse(serialized);
19+
20+
// `cleanedUp` is set synchronously when the cleanup function runs.
21+
// The async loop checks it after each await so any handle that resolves
22+
// after cleanup is unsubscribed immediately rather than being orphaned.
23+
let cleanedUp = false;
24+
const resolvedSubs: { unsubscribe(): void }[] = [];
25+
26+
(async () => {
27+
for (const options of currentStreams) {
28+
if (cleanedUp) break;
29+
const sub = await db.syncStream(options.name, options.parameters ?? undefined).subscribe(options);
30+
if (cleanedUp) {
31+
// Cleanup already ran while this subscribe was in flight — drop it immediately.
3132
sub.unsubscribe();
33+
break;
3234
}
33-
return;
35+
resolvedSubs.push(sub);
3436
}
37+
})();
3538

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]);
39+
return () => {
40+
cleanedUp = true;
41+
for (const sub of resolvedSubs) {
42+
sub.unsubscribe();
43+
}
44+
};
45+
}, [serialized]);
4646

4747
return useMemo(
4848
() =>

0 commit comments

Comments
 (0)