@syncular/client-react 0.0.2-2 → 0.0.3-6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,11 +15,20 @@ import type {
15
15
  MutationsCommitFn,
16
16
  MutationsTx,
17
17
  OutboxCommitMeta,
18
+ SubscriptionState,
19
+ SyncAwaitBootstrapOptions,
20
+ SyncAwaitPhaseOptions,
18
21
  SyncClientDb,
19
22
  SyncClientPlugin,
23
+ SyncDiagnostics,
20
24
  SyncOperation,
25
+ SyncProgress,
26
+ SyncRepairOptions,
27
+ SyncResetOptions,
28
+ SyncResetResult,
21
29
  SyncSubscriptionRequest,
22
30
  SyncTransport,
31
+ TransportHealth,
23
32
  } from '@syncular/client';
24
33
  import {
25
34
  type ConflictInfo,
@@ -126,6 +135,27 @@ export interface UseSyncEngineResult {
126
135
  disconnect: () => void;
127
136
  start: () => Promise<void>;
128
137
  resetLocalState: () => void;
138
+ getTransportHealth: () => Readonly<TransportHealth>;
139
+ getProgress: () => Promise<SyncProgress>;
140
+ getDiagnostics: () => Promise<SyncDiagnostics>;
141
+ listSubscriptionStates: (args?: {
142
+ stateId?: string;
143
+ table?: string;
144
+ status?: 'active' | 'revoked';
145
+ }) => Promise<SubscriptionState[]>;
146
+ getSubscriptionState: (
147
+ subscriptionId: string,
148
+ options?: { stateId?: string }
149
+ ) => Promise<SubscriptionState | null>;
150
+ reset: (options: SyncResetOptions) => Promise<SyncResetResult>;
151
+ repair: (options: SyncRepairOptions) => Promise<SyncResetResult>;
152
+ awaitPhase: (
153
+ phase: SyncProgress['channelPhase'],
154
+ options?: SyncAwaitPhaseOptions
155
+ ) => Promise<SyncProgress>;
156
+ awaitBootstrapComplete: (
157
+ options?: SyncAwaitBootstrapOptions
158
+ ) => Promise<SyncProgress>;
129
159
  }
130
160
 
131
161
  export interface SyncStatus {
@@ -133,12 +163,22 @@ export interface SyncStatus {
133
163
  isOnline: boolean;
134
164
  isSyncing: boolean;
135
165
  lastSyncAt: number | null;
166
+ lastSyncAgeMs: number | null;
167
+ isStale: boolean;
136
168
  pendingCount: number;
137
169
  error: SyncError | null;
138
170
  isRetrying: boolean;
139
171
  retryCount: number;
140
172
  }
141
173
 
174
+ export interface UseSyncStatusOptions {
175
+ /**
176
+ * Mark status as stale when `Date.now() - lastSyncAt` exceeds this value.
177
+ * If omitted, `isStale` is always false.
178
+ */
179
+ staleAfterMs?: number;
180
+ }
181
+
142
182
  export interface UseSyncConnectionResult {
143
183
  state: SyncConnectionState;
144
184
  mode: SyncTransportMode;
@@ -148,6 +188,45 @@ export interface UseSyncConnectionResult {
148
188
  disconnect: () => void;
149
189
  }
150
190
 
191
+ export interface UseTransportHealthResult {
192
+ health: TransportHealth;
193
+ }
194
+
195
+ export interface UseSyncProgressOptions {
196
+ /**
197
+ * Polling interval while bootstrapping.
198
+ * Set to 0 to disable interval refresh.
199
+ */
200
+ pollIntervalMs?: number;
201
+ }
202
+
203
+ export interface UseSyncProgressResult {
204
+ progress: SyncProgress | null;
205
+ isLoading: boolean;
206
+ error: Error | null;
207
+ refresh: () => Promise<void>;
208
+ }
209
+
210
+ export interface UseSyncSubscriptionsOptions {
211
+ stateId?: string;
212
+ table?: string;
213
+ status?: 'active' | 'revoked';
214
+ }
215
+
216
+ export interface UseSyncSubscriptionsResult {
217
+ subscriptions: SubscriptionState[];
218
+ isLoading: boolean;
219
+ error: Error | null;
220
+ refresh: () => Promise<void>;
221
+ }
222
+
223
+ export interface UseSyncSubscriptionResult {
224
+ subscription: SubscriptionState | null;
225
+ isLoading: boolean;
226
+ error: Error | null;
227
+ refresh: () => Promise<void>;
228
+ }
229
+
151
230
  export interface UseConflictsResult {
152
231
  conflicts: ConflictInfo[];
153
232
  count: number;
@@ -179,6 +258,8 @@ export interface UseSyncQueryResult<T> {
179
258
  data: T | undefined;
180
259
  isLoading: boolean;
181
260
  error: Error | null;
261
+ isStale: boolean;
262
+ lastSyncAt: number | null;
182
263
  refetch: () => Promise<void>;
183
264
  }
184
265
 
@@ -186,6 +267,9 @@ export interface UseSyncQueryOptions {
186
267
  enabled?: boolean;
187
268
  deps?: unknown[];
188
269
  keyField?: string;
270
+ watchTables?: string[];
271
+ pollIntervalMs?: number;
272
+ staleAfterMs?: number;
189
273
  }
190
274
 
191
275
  export interface UseQueryResult<T> {
@@ -506,6 +590,43 @@ export function createSyncularReact<DB extends SyncClientDb>() {
506
590
  () => engine.resetLocalState(),
507
591
  [engine]
508
592
  );
593
+ const getTransportHealth = useCallback(
594
+ () => engine.getTransportHealth(),
595
+ [engine]
596
+ );
597
+ const getProgress = useCallback(() => engine.getProgress(), [engine]);
598
+ const getDiagnostics = useCallback(() => engine.getDiagnostics(), [engine]);
599
+ const listSubscriptionStates = useCallback(
600
+ (args?: {
601
+ stateId?: string;
602
+ table?: string;
603
+ status?: 'active' | 'revoked';
604
+ }) => engine.listSubscriptionStates(args),
605
+ [engine]
606
+ );
607
+ const getSubscriptionState = useCallback(
608
+ (subscriptionId: string, options?: { stateId?: string }) =>
609
+ engine.getSubscriptionState(subscriptionId, options),
610
+ [engine]
611
+ );
612
+ const reset = useCallback(
613
+ (options: SyncResetOptions) => engine.reset(options),
614
+ [engine]
615
+ );
616
+ const repair = useCallback(
617
+ (options: SyncRepairOptions) => engine.repair(options),
618
+ [engine]
619
+ );
620
+ const awaitPhase = useCallback(
621
+ (phase: SyncProgress['channelPhase'], options?: SyncAwaitPhaseOptions) =>
622
+ engine.awaitPhase(phase, options),
623
+ [engine]
624
+ );
625
+ const awaitBootstrapComplete = useCallback(
626
+ (options?: SyncAwaitBootstrapOptions) =>
627
+ engine.awaitBootstrapComplete(options),
628
+ [engine]
629
+ );
509
630
 
510
631
  return {
511
632
  state,
@@ -514,10 +635,20 @@ export function createSyncularReact<DB extends SyncClientDb>() {
514
635
  disconnect,
515
636
  start,
516
637
  resetLocalState,
638
+ getTransportHealth,
639
+ getProgress,
640
+ getDiagnostics,
641
+ listSubscriptionStates,
642
+ getSubscriptionState,
643
+ reset,
644
+ repair,
645
+ awaitPhase,
646
+ awaitBootstrapComplete,
517
647
  };
518
648
  }
519
649
 
520
- function useSyncStatus(): SyncStatus {
650
+ function useSyncStatus(options: UseSyncStatusOptions = {}): SyncStatus {
651
+ const { staleAfterMs } = options;
521
652
  const engine = useEngine();
522
653
 
523
654
  const state = useSyncExternalStore(
@@ -526,19 +657,44 @@ export function createSyncularReact<DB extends SyncClientDb>() {
526
657
  useCallback(() => engine.getState(), [engine])
527
658
  );
528
659
 
529
- return useMemo<SyncStatus>(
530
- () => ({
660
+ const [staleClock, setStaleClock] = useState<number>(Date.now());
661
+
662
+ useEffect(() => {
663
+ if (staleAfterMs === undefined || staleAfterMs <= 0) return;
664
+
665
+ const intervalMs = Math.min(
666
+ 1000,
667
+ Math.max(100, Math.floor(staleAfterMs / 2))
668
+ );
669
+ const timer = setInterval(() => {
670
+ setStaleClock(Date.now());
671
+ }, intervalMs);
672
+
673
+ return () => clearInterval(timer);
674
+ }, [staleAfterMs]);
675
+
676
+ return useMemo<SyncStatus>(() => {
677
+ const now = staleAfterMs !== undefined ? staleClock : Date.now();
678
+ const lastSyncAgeMs =
679
+ state.lastSyncAt === null ? null : Math.max(0, now - state.lastSyncAt);
680
+ const isStale =
681
+ staleAfterMs !== undefined && staleAfterMs > 0
682
+ ? state.lastSyncAt === null || (lastSyncAgeMs ?? 0) > staleAfterMs
683
+ : false;
684
+
685
+ return {
531
686
  enabled: state.enabled,
532
687
  isOnline: state.connectionState === 'connected',
533
688
  isSyncing: state.isSyncing,
534
689
  lastSyncAt: state.lastSyncAt,
690
+ lastSyncAgeMs,
691
+ isStale,
535
692
  pendingCount: state.pendingCount,
536
693
  error: state.error,
537
694
  isRetrying: state.isRetrying,
538
695
  retryCount: state.retryCount,
539
- }),
540
- [state]
541
- );
696
+ };
697
+ }, [state, staleAfterMs, staleClock]);
542
698
  }
543
699
 
544
700
  function useSyncConnection(): UseSyncConnectionResult {
@@ -571,6 +727,220 @@ export function createSyncularReact<DB extends SyncClientDb>() {
571
727
  );
572
728
  }
573
729
 
730
+ function useTransportHealth(): UseTransportHealthResult {
731
+ const engine = useEngine();
732
+
733
+ const health = useSyncExternalStore(
734
+ useCallback(
735
+ (callback) => {
736
+ const unsubscribers = [
737
+ engine.subscribe(callback),
738
+ engine.on('connection:change', callback),
739
+ engine.on('sync:complete', callback),
740
+ engine.on('sync:error', callback),
741
+ ];
742
+ return () => {
743
+ for (const unsubscribe of unsubscribers) unsubscribe();
744
+ };
745
+ },
746
+ [engine]
747
+ ),
748
+ useCallback(() => engine.getTransportHealth(), [engine]),
749
+ useCallback(() => engine.getTransportHealth(), [engine])
750
+ );
751
+
752
+ return useMemo(() => ({ health }), [health]);
753
+ }
754
+
755
+ function useSyncProgress(
756
+ options: UseSyncProgressOptions = {}
757
+ ): UseSyncProgressResult {
758
+ const engine = useEngine();
759
+ const { pollIntervalMs = 500 } = options;
760
+
761
+ const [progress, setProgress] = useState<SyncProgress | null>(null);
762
+ const [isLoading, setIsLoading] = useState(true);
763
+ const [error, setError] = useState<Error | null>(null);
764
+ const loadedRef = useRef(false);
765
+
766
+ const refresh = useCallback(async () => {
767
+ if (!loadedRef.current) {
768
+ setIsLoading(true);
769
+ }
770
+
771
+ try {
772
+ const next = await engine.getProgress();
773
+ setProgress(next);
774
+ setError(null);
775
+ } catch (err) {
776
+ setError(err instanceof Error ? err : new Error(String(err)));
777
+ } finally {
778
+ loadedRef.current = true;
779
+ setIsLoading(false);
780
+ }
781
+ }, [engine]);
782
+
783
+ useEffect(() => {
784
+ void refresh();
785
+ }, [refresh]);
786
+
787
+ useEffect(() => {
788
+ const unsubscribers = [
789
+ engine.on('sync:start', refresh),
790
+ engine.on('sync:complete', refresh),
791
+ engine.on('sync:error', refresh),
792
+ engine.on('bootstrap:start', refresh),
793
+ engine.on('bootstrap:progress', refresh),
794
+ engine.on('bootstrap:complete', refresh),
795
+ ];
796
+ return () => {
797
+ for (const unsubscribe of unsubscribers) unsubscribe();
798
+ };
799
+ }, [engine, refresh]);
800
+
801
+ useEffect(() => {
802
+ if (pollIntervalMs <= 0) return;
803
+ if (progress?.channelPhase !== 'bootstrapping') return;
804
+
805
+ const timer = setInterval(() => {
806
+ void refresh();
807
+ }, pollIntervalMs);
808
+
809
+ return () => clearInterval(timer);
810
+ }, [pollIntervalMs, progress?.channelPhase, refresh]);
811
+
812
+ return useMemo(
813
+ () => ({
814
+ progress,
815
+ isLoading,
816
+ error,
817
+ refresh,
818
+ }),
819
+ [progress, isLoading, error, refresh]
820
+ );
821
+ }
822
+
823
+ function useSyncSubscriptions(
824
+ options: UseSyncSubscriptionsOptions = {}
825
+ ): UseSyncSubscriptionsResult {
826
+ const engine = useEngine();
827
+ const { stateId, table, status } = options;
828
+
829
+ const [subscriptions, setSubscriptions] = useState<SubscriptionState[]>([]);
830
+ const [isLoading, setIsLoading] = useState(true);
831
+ const [error, setError] = useState<Error | null>(null);
832
+ const loadedRef = useRef(false);
833
+
834
+ const refresh = useCallback(async () => {
835
+ if (!loadedRef.current) {
836
+ setIsLoading(true);
837
+ }
838
+
839
+ try {
840
+ const next = await engine.listSubscriptionStates({
841
+ stateId,
842
+ table,
843
+ status,
844
+ });
845
+ setSubscriptions(next);
846
+ setError(null);
847
+ } catch (err) {
848
+ setError(err instanceof Error ? err : new Error(String(err)));
849
+ } finally {
850
+ loadedRef.current = true;
851
+ setIsLoading(false);
852
+ }
853
+ }, [engine, stateId, table, status]);
854
+
855
+ useEffect(() => {
856
+ void refresh();
857
+ }, [refresh]);
858
+
859
+ useEffect(() => {
860
+ const unsubscribers = [
861
+ engine.on('sync:complete', refresh),
862
+ engine.on('sync:error', refresh),
863
+ engine.on('bootstrap:start', refresh),
864
+ engine.on('bootstrap:progress', refresh),
865
+ engine.on('bootstrap:complete', refresh),
866
+ ];
867
+ return () => {
868
+ for (const unsubscribe of unsubscribers) unsubscribe();
869
+ };
870
+ }, [engine, refresh]);
871
+
872
+ return useMemo(
873
+ () => ({
874
+ subscriptions,
875
+ isLoading,
876
+ error,
877
+ refresh,
878
+ }),
879
+ [subscriptions, isLoading, error, refresh]
880
+ );
881
+ }
882
+
883
+ function useSyncSubscription(
884
+ subscriptionId: string,
885
+ options: { stateId?: string } = {}
886
+ ): UseSyncSubscriptionResult {
887
+ const engine = useEngine();
888
+ const { stateId } = options;
889
+
890
+ const [subscription, setSubscription] = useState<SubscriptionState | null>(
891
+ null
892
+ );
893
+ const [isLoading, setIsLoading] = useState(true);
894
+ const [error, setError] = useState<Error | null>(null);
895
+ const loadedRef = useRef(false);
896
+
897
+ const refresh = useCallback(async () => {
898
+ if (!loadedRef.current) {
899
+ setIsLoading(true);
900
+ }
901
+
902
+ try {
903
+ const next = await engine.getSubscriptionState(subscriptionId, {
904
+ stateId,
905
+ });
906
+ setSubscription(next);
907
+ setError(null);
908
+ } catch (err) {
909
+ setError(err instanceof Error ? err : new Error(String(err)));
910
+ } finally {
911
+ loadedRef.current = true;
912
+ setIsLoading(false);
913
+ }
914
+ }, [engine, stateId, subscriptionId]);
915
+
916
+ useEffect(() => {
917
+ void refresh();
918
+ }, [refresh]);
919
+
920
+ useEffect(() => {
921
+ const unsubscribers = [
922
+ engine.on('sync:complete', refresh),
923
+ engine.on('sync:error', refresh),
924
+ engine.on('bootstrap:start', refresh),
925
+ engine.on('bootstrap:progress', refresh),
926
+ engine.on('bootstrap:complete', refresh),
927
+ ];
928
+ return () => {
929
+ for (const unsubscribe of unsubscribers) unsubscribe();
930
+ };
931
+ }, [engine, refresh]);
932
+
933
+ return useMemo(
934
+ () => ({
935
+ subscription,
936
+ isLoading,
937
+ error,
938
+ refresh,
939
+ }),
940
+ [subscription, isLoading, error, refresh]
941
+ );
942
+ }
943
+
574
944
  function useConflicts(): UseConflictsResult {
575
945
  const engine = useEngine();
576
946
 
@@ -694,9 +1064,17 @@ export function createSyncularReact<DB extends SyncClientDb>() {
694
1064
  ) => ExecutableQuery<TResult> | Promise<TResult>,
695
1065
  options: UseSyncQueryOptions = {}
696
1066
  ): UseSyncQueryResult<TResult> {
697
- const { enabled = true, deps = [], keyField = 'id' } = options;
1067
+ const {
1068
+ enabled = true,
1069
+ deps = [],
1070
+ keyField = 'id',
1071
+ watchTables = [],
1072
+ pollIntervalMs,
1073
+ staleAfterMs,
1074
+ } = options;
698
1075
  const { db } = useSyncContext();
699
1076
  const engine = useEngine();
1077
+ const watchTablesSet = useMemo(() => new Set(watchTables), [watchTables]);
700
1078
 
701
1079
  const queryFnRef = useRef<typeof queryFn>(queryFn);
702
1080
  queryFnRef.current = queryFn;
@@ -704,6 +1082,10 @@ export function createSyncularReact<DB extends SyncClientDb>() {
704
1082
  const [data, setData] = useState<TResult | undefined>(undefined);
705
1083
  const [isLoading, setIsLoading] = useState(true);
706
1084
  const [error, setError] = useState<Error | null>(null);
1085
+ const [lastSyncAt, setLastSyncAt] = useState<number | null>(
1086
+ () => engine.getState().lastSyncAt
1087
+ );
1088
+ const [staleClock, setStaleClock] = useState<number>(Date.now());
707
1089
 
708
1090
  const versionRef = useRef(0);
709
1091
  const watchedScopesRef = useRef<Set<string>>(new Set());
@@ -717,6 +1099,7 @@ export function createSyncularReact<DB extends SyncClientDb>() {
717
1099
  previousFingerprintRef.current = 'disabled';
718
1100
  setData(undefined);
719
1101
  }
1102
+ setLastSyncAt(engine.getState().lastSyncAt);
720
1103
  setIsLoading(false);
721
1104
  hasLoadedRef.current = true;
722
1105
  return;
@@ -746,6 +1129,7 @@ export function createSyncularReact<DB extends SyncClientDb>() {
746
1129
 
747
1130
  if (version === versionRef.current) {
748
1131
  watchedScopesRef.current = scopeCollector;
1132
+ setLastSyncAt(engine.getState().lastSyncAt);
749
1133
 
750
1134
  const fingerprint = fingerprintCollectorRef.current.getCombined();
751
1135
  if (
@@ -774,10 +1158,20 @@ export function createSyncularReact<DB extends SyncClientDb>() {
774
1158
  // eslint-disable-next-line react-hooks/exhaustive-deps
775
1159
  }, [executeQuery, ...deps]);
776
1160
 
1161
+ useEffect(() => {
1162
+ const unsubscribe = engine.subscribe(() => {
1163
+ const nextLastSyncAt = engine.getState().lastSyncAt;
1164
+ setLastSyncAt((previous) =>
1165
+ previous === nextLastSyncAt ? previous : nextLastSyncAt
1166
+ );
1167
+ });
1168
+ return unsubscribe;
1169
+ }, [engine]);
1170
+
777
1171
  useEffect(() => {
778
1172
  if (!enabled) return;
779
1173
  const unsubscribe = engine.on('sync:complete', () => {
780
- executeQuery();
1174
+ void executeQuery();
781
1175
  });
782
1176
  return unsubscribe;
783
1177
  }, [engine, enabled, executeQuery]);
@@ -786,35 +1180,75 @@ export function createSyncularReact<DB extends SyncClientDb>() {
786
1180
  if (!enabled) return;
787
1181
 
788
1182
  const unsubscribe = engine.on('data:change', (event) => {
789
- const changedScopes = event.scopes || [];
1183
+ const changedScopes = event.scopes ?? [];
790
1184
  const watchedScopes = watchedScopesRef.current;
1185
+ const hasDynamicFilter = watchedScopes.size > 0;
1186
+ const hasTableFilter = watchTablesSet.size > 0;
791
1187
 
792
- if (watchedScopes.size > 0) {
793
- const hasWatchedScope = changedScopes.some((s) =>
794
- watchedScopes.has(s)
1188
+ if (hasDynamicFilter || hasTableFilter) {
1189
+ const matchesDynamic = changedScopes.some((scope) =>
1190
+ watchedScopes.has(scope)
795
1191
  );
796
- if (!hasWatchedScope) return;
1192
+ const matchesConfigured = changedScopes.some((scope) =>
1193
+ watchTablesSet.has(scope)
1194
+ );
1195
+
1196
+ if (!matchesDynamic && !matchesConfigured) {
1197
+ return;
1198
+ }
797
1199
  }
798
1200
 
799
- executeQuery();
1201
+ void executeQuery();
800
1202
  });
801
1203
 
802
1204
  return unsubscribe;
803
- }, [engine, enabled, executeQuery]);
1205
+ }, [engine, enabled, executeQuery, watchTablesSet]);
1206
+
1207
+ useEffect(() => {
1208
+ if (!enabled) return;
1209
+ if (pollIntervalMs === undefined || pollIntervalMs <= 0) return;
1210
+
1211
+ const timer = setInterval(() => {
1212
+ void executeQuery();
1213
+ }, pollIntervalMs);
1214
+
1215
+ return () => clearInterval(timer);
1216
+ }, [enabled, pollIntervalMs, executeQuery]);
1217
+
1218
+ useEffect(() => {
1219
+ if (staleAfterMs === undefined || staleAfterMs <= 0) return;
1220
+
1221
+ const intervalMs = Math.min(
1222
+ 1000,
1223
+ Math.max(100, Math.floor(staleAfterMs / 2))
1224
+ );
1225
+ const timer = setInterval(() => {
1226
+ setStaleClock(Date.now());
1227
+ }, intervalMs);
1228
+
1229
+ return () => clearInterval(timer);
1230
+ }, [staleAfterMs]);
804
1231
 
805
1232
  const refetch = useCallback(async () => {
806
1233
  await executeQuery();
807
1234
  }, [executeQuery]);
808
1235
 
809
- return useMemo(
810
- () => ({
1236
+ return useMemo(() => {
1237
+ const now = staleAfterMs !== undefined ? staleClock : Date.now();
1238
+ const isStale =
1239
+ staleAfterMs !== undefined && staleAfterMs > 0
1240
+ ? lastSyncAt === null || now - lastSyncAt > staleAfterMs
1241
+ : false;
1242
+
1243
+ return {
811
1244
  data,
812
1245
  isLoading,
813
1246
  error,
1247
+ isStale,
1248
+ lastSyncAt,
814
1249
  refetch,
815
- }),
816
- [data, isLoading, error, refetch]
817
- );
1250
+ };
1251
+ }, [data, isLoading, error, staleAfterMs, staleClock, lastSyncAt, refetch]);
818
1252
  }
819
1253
 
820
1254
  function useQuery<TResult>(
@@ -1382,6 +1816,10 @@ export function createSyncularReact<DB extends SyncClientDb>() {
1382
1816
  useSyncEngine,
1383
1817
  useSyncStatus,
1384
1818
  useSyncConnection,
1819
+ useTransportHealth,
1820
+ useSyncProgress,
1821
+ useSyncSubscriptions,
1822
+ useSyncSubscription,
1385
1823
  useSyncQuery,
1386
1824
  useQuery,
1387
1825
  useMutation,
package/src/index.ts CHANGED
@@ -29,8 +29,22 @@ export type {
29
29
  UseResolveConflictResult,
30
30
  UseSyncConnectionResult,
31
31
  UseSyncEngineResult,
32
+ UseSyncProgressOptions,
33
+ UseSyncProgressResult,
32
34
  UseSyncQueryOptions,
33
35
  UseSyncQueryResult,
36
+ UseSyncStatusOptions,
37
+ UseSyncSubscriptionResult,
38
+ UseSyncSubscriptionsOptions,
39
+ UseSyncSubscriptionsResult,
40
+ UseTransportHealthResult,
34
41
  } from './createSyncularReact';
35
42
  // Re-export core client types for convenience.
36
43
  export { createSyncularReact } from './createSyncularReact';
44
+ export type {
45
+ SyncGroupChannel,
46
+ SyncGroupChannelSnapshot,
47
+ SyncGroupStatus,
48
+ UseSyncGroupResult,
49
+ } from './useSyncGroup';
50
+ export { useSyncGroup } from './useSyncGroup';