|
| 1 | +import { useEffect, useMemo, useState } from 'react'; |
| 2 | +import { usePowerSync } from './PowerSyncContext.js'; |
| 3 | +import { |
| 4 | + AbstractPowerSyncDatabase, |
| 5 | + SyncStatus, |
| 6 | + SyncStreamStatus, |
| 7 | + SyncStreamSubscribeOptions, |
| 8 | + SyncStreamSubscription |
| 9 | +} from '@powersync/common'; |
| 10 | +import { useStatus } from './useStatus.js'; |
| 11 | +import { QuerySyncStreamOptions } from './watched/watch-types.js'; |
| 12 | + |
| 13 | +/** |
| 14 | + * A sync stream to subscribe to in {@link useSyncStream}. |
| 15 | + * |
| 16 | + * For more details on sync streams, see the [documentation](https://docs.powersync.com/usage/sync-streams). |
| 17 | + */ |
| 18 | +export interface UseSyncStreamOptions extends SyncStreamSubscribeOptions { |
| 19 | + /** |
| 20 | + * The name of the stream to subscribe to. |
| 21 | + */ |
| 22 | + name: string; |
| 23 | + /** |
| 24 | + * Parameters for the stream subscription. A single stream can have multiple subscriptions with different parameter |
| 25 | + * sets. |
| 26 | + */ |
| 27 | + parameters?: Record<string, any>; |
| 28 | +} |
| 29 | + |
| 30 | +/** |
| 31 | + * Creates a PowerSync stream subscription. The subscription is kept alive as long as the React component calling this |
| 32 | + * function. When it unmounts, {@link SyncStreamSubscription.unsubscribe} is called |
| 33 | + * |
| 34 | + * For more details on sync streams, see the [documentation](https://docs.powersync.com/usage/sync-streams). |
| 35 | + * |
| 36 | + * @returns The status for that stream, or `null` if the stream is currently being resolved. |
| 37 | + */ |
| 38 | +export function useSyncStream(options: UseSyncStreamOptions): SyncStreamStatus | null { |
| 39 | + const { name, parameters } = options; |
| 40 | + const db = usePowerSync(); |
| 41 | + const status = useStatus(); |
| 42 | + const [subscription, setSubscription] = useState<SyncStreamSubscription | null>(null); |
| 43 | + |
| 44 | + useEffect(() => { |
| 45 | + let active = true; |
| 46 | + let subscription: SyncStreamSubscription | null = null; |
| 47 | + |
| 48 | + db.syncStream(name, parameters) |
| 49 | + .subscribe(options) |
| 50 | + .then((sub) => { |
| 51 | + if (active) { |
| 52 | + subscription = sub; |
| 53 | + setSubscription(sub); |
| 54 | + } else { |
| 55 | + // The cleanup function already ran, unsubscribe immediately. |
| 56 | + sub.unsubscribe(); |
| 57 | + } |
| 58 | + }); |
| 59 | + |
| 60 | + return () => { |
| 61 | + active = false; |
| 62 | + // If we don't have a subscription yet, it'll still get cleaned up once the promise resolves because we've set |
| 63 | + // active to false. |
| 64 | + subscription?.unsubscribe(); |
| 65 | + }; |
| 66 | + }, [name, parameters]); |
| 67 | + |
| 68 | + return subscription && status.forStream(subscription); |
| 69 | +} |
| 70 | + |
| 71 | +/** |
| 72 | + * @internal |
| 73 | + */ |
| 74 | +export function useAllSyncStreamsHaveSynced( |
| 75 | + db: AbstractPowerSyncDatabase, |
| 76 | + streams: QuerySyncStreamOptions[] | undefined |
| 77 | +): boolean { |
| 78 | + // Since streams are a user-supplied array, they will likely be different each time this function is called. We don't |
| 79 | + // want to update underlying subscriptions each time, though. |
| 80 | + const hash = useMemo(() => streams && JSON.stringify(streams), [streams]); |
| 81 | + const [synced, setHasSynced] = useState(streams == null || streams.every((e) => e.waitForStream != true)); |
| 82 | + |
| 83 | + useEffect(() => { |
| 84 | + if (streams) { |
| 85 | + setHasSynced(false); |
| 86 | + |
| 87 | + const promises: Promise<SyncStreamSubscription>[] = []; |
| 88 | + const abort = new AbortController(); |
| 89 | + for (const stream of streams) { |
| 90 | + promises.push(db.syncStream(stream.name, stream.parameters).subscribe(stream)); |
| 91 | + } |
| 92 | + |
| 93 | + // First, wait for all subscribe() calls to make all subscriptions active. |
| 94 | + Promise.all(promises).then(async (resolvedStreams) => { |
| 95 | + function allHaveSynced(status: SyncStatus) { |
| 96 | + return resolvedStreams.every((s, i) => { |
| 97 | + const request = streams[i]; |
| 98 | + return !request.waitForStream || status.forStream(s)?.subscription?.hasSynced; |
| 99 | + }); |
| 100 | + } |
| 101 | + |
| 102 | + // Wait for the effect to be cancelled or all streams having synced. |
| 103 | + await db.waitForStatus(allHaveSynced, abort.signal); |
| 104 | + if (abort.signal.aborted) { |
| 105 | + // Was cancelled |
| 106 | + } else { |
| 107 | + // Has synced, update public state. |
| 108 | + setHasSynced(true); |
| 109 | + |
| 110 | + // Wait for cancellation before clearing subscriptions. |
| 111 | + await new Promise<void>((resolve) => { |
| 112 | + abort.signal.addEventListener('abort', () => resolve()); |
| 113 | + }); |
| 114 | + } |
| 115 | + |
| 116 | + // Effect was definitely cancelled at this point, so drop the subscriptions. |
| 117 | + for (const stream of resolvedStreams) { |
| 118 | + stream.unsubscribe(); |
| 119 | + } |
| 120 | + }); |
| 121 | + |
| 122 | + return () => abort.abort(); |
| 123 | + } else { |
| 124 | + // There are no streams, so all of them have synced. |
| 125 | + setHasSynced(true); |
| 126 | + return undefined; |
| 127 | + } |
| 128 | + }, [hash]); |
| 129 | + |
| 130 | + return synced; |
| 131 | +} |
0 commit comments