@tanstack/react-db 0.5.1 → 0.5.3

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.
@@ -5,16 +5,17 @@ import {
5
5
  BaseQueryBuilder,
6
6
  IR,
7
7
  UnhashableQueryIRError,
8
- createLiveQueryCollection,
9
8
  createLiveQueryObserver,
10
9
  deepEquals,
11
10
  getPreparedLiveQueryIdentity,
12
11
  getStableValueHash,
13
12
  isCollection,
14
13
  prepareLiveQueryValue,
14
+ resolveLiveQueryValue,
15
15
  } from '@tanstack/db'
16
16
  import { useOptionalDbClient } from './DbProvider'
17
17
  import { setLiveQueryResultInfo } from './live-query-internals'
18
+ import { shouldWarnInDevelopment } from './development'
18
19
  import type {
19
20
  Collection,
20
21
  CollectionImpl,
@@ -172,16 +173,6 @@ export function warnDeprecatedDepsArray(
172
173
  )
173
174
  }
174
175
 
175
- function shouldWarnInDevelopment(disableEnvVar: string): boolean {
176
- if (typeof process === `undefined`) {
177
- return false
178
- }
179
-
180
- return (
181
- process.env.NODE_ENV !== `production` && process.env[disableEnvVar] !== `1`
182
- )
183
- }
184
-
185
176
  function getCurrentTime(): number {
186
177
  return typeof performance !== `undefined` &&
187
178
  typeof performance.now === `function`
@@ -336,40 +327,6 @@ export function warnUnhashableDerivedIdentity(
336
327
  )
337
328
  }
338
329
 
339
- function createCollectionFromPreparedQuery(
340
- value: unknown,
341
- defaultGcTime = DEFAULT_GC_TIME_MS,
342
- ) {
343
- if (value === undefined || value === null) {
344
- return null
345
- }
346
-
347
- if (isCollection(value)) {
348
- value.startSyncImmediate()
349
- return value
350
- }
351
-
352
- if (value instanceof BaseQueryBuilder) {
353
- return createLiveQueryCollection({
354
- query: value,
355
- startSync: true,
356
- gcTime: defaultGcTime,
357
- })
358
- }
359
-
360
- if (typeof value === `object`) {
361
- return createLiveQueryCollection({
362
- startSync: true,
363
- gcTime: defaultGcTime,
364
- ...(value as LiveQueryCollectionConfig<any>),
365
- })
366
- }
367
-
368
- throw new Error(
369
- `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof value}`,
370
- )
371
- }
372
-
373
330
  /**
374
331
  * Create a live query using a query function.
375
332
  * @param queryFn - Query function that defines what data to fetch
@@ -771,6 +728,32 @@ export function useLiveQueryForSuspense(
771
728
  return useLiveQueryImpl(configOrQueryOrCollection, deps, true)
772
729
  }
773
730
 
731
+ // What one hook instance keeps across renders. It lives in a single ref
732
+ // slot, so a render neither looks up nor allocates more.
733
+ function createHookInstance(dbClient: DbClient | undefined) {
734
+ return {
735
+ collection: null as Collection<object, string | number, {}> | null,
736
+ deps: null as Array<unknown> | null,
737
+ config: null as unknown,
738
+ client: dbClient,
739
+ legacyUnhashableIdentity: [`legacy-unhashable`] as Array<unknown>,
740
+ derivedIdentityProfiler: {
741
+ renderCount: 0,
742
+ totalMs: 0,
743
+ maxMs: 0,
744
+ warned: false,
745
+ } as DerivedIdentityProfiler,
746
+ deferredCollections: new Set<
747
+ CollectionImpl<any, string | number, any, any, any>
748
+ >(),
749
+ observer: null as LiveQueryObserver<object, string | number> | null,
750
+ queryHash: undefined as string | undefined,
751
+ suspenseKey: undefined as string | undefined,
752
+ identityError: undefined as UnhashableQueryIRError | undefined,
753
+ subscribe: null as ((onStoreChange: () => void) => () => void) | null,
754
+ }
755
+ }
756
+
774
757
  function useLiveQueryImpl(
775
758
  configOrQueryOrCollection: any,
776
759
  deps: Array<unknown> | undefined,
@@ -784,32 +767,8 @@ function useLiveQueryImpl(
784
767
  : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)
785
768
  const resolvedDeps = deps ?? []
786
769
 
787
- // Use refs to cache collection and track dependencies
788
- const collectionRef = useRef<Collection<object, string | number, {}> | null>(
789
- null,
790
- )
791
- const depsRef = useRef<Array<unknown> | null>(null)
792
- const configRef = useRef<unknown>(null)
793
- const clientRef = useRef(dbClient)
794
- const legacyUnhashableIdentityRef = useRef<Array<unknown>>([
795
- `legacy-unhashable`,
796
- ])
797
-
798
- const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({
799
- renderCount: 0,
800
- totalMs: 0,
801
- maxMs: 0,
802
- warned: false,
803
- })
804
- const deferredCollectionsRef = useRef(
805
- new Set<CollectionImpl<any, string | number, any, any, any>>(),
806
- )
807
- const observerRef = useRef<LiveQueryObserver<object, string | number> | null>(
808
- null,
809
- )
810
- const queryHashRef = useRef<string | undefined>(undefined)
811
- const suspenseKeyRef = useRef<string | undefined>(undefined)
812
- const identityErrorRef = useRef<UnhashableQueryIRError | undefined>(undefined)
770
+ const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)
771
+ const instance = (instanceRef.current ??= createHookInstance(dbClient))
813
772
 
814
773
  const queryKey = !inputIsCollection
815
774
  ? getExplicitQueryKey(configOrQueryOrCollection)
@@ -825,22 +784,29 @@ function useLiveQueryImpl(
825
784
  streamIdentity = [`queryKey`, queryKey]
826
785
  } else if (deps !== undefined) {
827
786
  identityDeps = resolvedDeps
828
- try {
829
- preparedQueryValue = prepareQueryValue(
830
- configOrQueryOrCollection,
831
- dbClient,
832
- deferredCollectionsRef.current,
833
- )
834
- streamIdentity = [
835
- `deps`,
836
- resolvedDeps,
837
- getPreparedLiveQueryIdentity(preparedQueryValue),
838
- ]
839
- } catch (error) {
840
- if (!(error instanceof UnhashableQueryIRError)) throw error
841
- warnUnhashableDerivedIdentity(error)
842
- identityError = error
843
- }
787
+ // Deps decide reuse. Only hydration and Suspense read the query hash; the
788
+ // development warning about unhashable queries still derives it.
789
+ if (
790
+ dbClient ||
791
+ forSuspense ||
792
+ shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)
793
+ )
794
+ try {
795
+ preparedQueryValue = prepareQueryValue(
796
+ configOrQueryOrCollection,
797
+ dbClient,
798
+ instance.deferredCollections,
799
+ )
800
+ streamIdentity = [
801
+ `deps`,
802
+ resolvedDeps,
803
+ getPreparedLiveQueryIdentity(preparedQueryValue),
804
+ ]
805
+ } catch (error) {
806
+ if (!(error instanceof UnhashableQueryIRError)) throw error
807
+ warnUnhashableDerivedIdentity(error)
808
+ identityError = error
809
+ }
844
810
  } else if (inputIsCollection) {
845
811
  identityDeps = []
846
812
  streamIdentity = [`collection`, configOrQueryOrCollection.id]
@@ -848,8 +814,8 @@ function useLiveQueryImpl(
848
814
  const preparation = prepareDerivedQuery(
849
815
  configOrQueryOrCollection,
850
816
  dbClient,
851
- derivedIdentityProfilerRef.current,
852
- deferredCollectionsRef.current,
817
+ instance.derivedIdentityProfiler,
818
+ instance.deferredCollections,
853
819
  )
854
820
  preparedQueryValue = preparation.value
855
821
  if (preparation.status === `hashable`) {
@@ -857,7 +823,7 @@ function useLiveQueryImpl(
857
823
  streamIdentity = preparation.identityDeps
858
824
  } else {
859
825
  warnUnhashableDerivedIdentity(preparation.error)
860
- identityDeps = legacyUnhashableIdentityRef.current
826
+ identityDeps = instance.legacyUnhashableIdentity
861
827
  identityError = preparation.error
862
828
  }
863
829
  }
@@ -885,10 +851,10 @@ function useLiveQueryImpl(
885
851
  !inputIsCollection &&
886
852
  queryHash !== undefined &&
887
853
  !dbClient &&
888
- collectionRef.current !== null &&
889
- clientRef.current === dbClient &&
890
- queryHashRef.current === queryHash &&
891
- suspenseKeyRef.current !== undefined
854
+ instance.collection !== null &&
855
+ instance.client === dbClient &&
856
+ instance.queryHash === queryHash &&
857
+ instance.suspenseKey !== undefined
892
858
 
893
859
  if (
894
860
  forSuspense &&
@@ -901,14 +867,14 @@ function useLiveQueryImpl(
901
867
  preparedQueryValue = prepareQueryValue(
902
868
  configOrQueryOrCollection,
903
869
  dbClient,
904
- deferredCollectionsRef.current,
870
+ instance.deferredCollections,
905
871
  )
906
872
  }
907
873
 
908
874
  const suspenseKey =
909
875
  queryHash && !dbClient
910
876
  ? canReuseSuspenseKey
911
- ? suspenseKeyRef.current
877
+ ? instance.suspenseKey
912
878
  : getUnscopedSuspenseKey(preparedQueryValue, queryHash)
913
879
  : queryHash
914
880
 
@@ -922,23 +888,24 @@ function useLiveQueryImpl(
922
888
  const suspenseCollection = suspenseEntry?.collection
923
889
 
924
890
  const identityChanged =
925
- depsRef.current === null ||
891
+ instance.deps === null ||
926
892
  (deps !== undefined
927
- ? depsRef.current.length !== identityDeps.length ||
928
- depsRef.current.some((dep, index) => dep !== identityDeps[index])
929
- : !deepEquals(depsRef.current, identityDeps))
893
+ ? instance.deps.length !== identityDeps.length ||
894
+ instance.deps.some((dep, index) => dep !== identityDeps[index])
895
+ : !deepEquals(instance.deps, identityDeps))
930
896
 
931
897
  // Check if we need to create/recreate the collection
932
898
  const needsNewCollection =
933
- !collectionRef.current ||
934
- (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||
935
- (!inputIsCollection && (clientRef.current !== dbClient || identityChanged))
899
+ !instance.collection ||
900
+ (inputIsCollection && instance.config !== configOrQueryOrCollection) ||
901
+ (!inputIsCollection && (instance.client !== dbClient || identityChanged))
936
902
 
937
903
  const resumeDeferredCollections = () => {
938
- for (const collection of deferredCollectionsRef.current) {
904
+ if (instance.deferredCollections.size === 0) return
905
+ for (const collection of instance.deferredCollections) {
939
906
  collection._resumeSyncStart()
940
907
  }
941
- deferredCollectionsRef.current.clear()
908
+ instance.deferredCollections.clear()
942
909
  }
943
910
 
944
911
  if (needsNewCollection) {
@@ -963,25 +930,28 @@ function useLiveQueryImpl(
963
930
  }
964
931
  // It's already a collection, ensure sync is started for React hooks
965
932
  configOrQueryOrCollection.startSyncImmediate()
966
- collectionRef.current = configOrQueryOrCollection
967
- configRef.current = configOrQueryOrCollection
933
+ instance.collection = configOrQueryOrCollection
934
+ instance.config = configOrQueryOrCollection
968
935
  } else {
969
936
  if (suspenseCollection) {
970
- collectionRef.current = suspenseCollection
937
+ instance.collection = suspenseCollection
971
938
  } else {
972
939
  if (preparedQueryValue === unpreparedQueryValue) {
973
940
  preparedQueryValue = prepareQueryValue(
974
941
  configOrQueryOrCollection,
975
942
  dbClient,
976
- deferredCollectionsRef.current,
943
+ instance.deferredCollections,
977
944
  )
978
945
  }
979
- collectionRef.current = createCollectionFromPreparedQuery(
980
- preparedQueryValue,
981
- forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS,
982
- ) as SuspenseCollection | null
983
- if (suspenseCollections && suspenseKey && collectionRef.current) {
984
- const collection = collectionRef.current
946
+ instance.collection = resolveLiveQueryValue(preparedQueryValue, {
947
+ gcTime: forSuspense
948
+ ? DEFAULT_SUSPENSE_GC_TIME_MS
949
+ : DEFAULT_GC_TIME_MS,
950
+ // Hydration and Suspense key the live-query Collection by identity.
951
+ pool: !forSuspense && !dbClient,
952
+ }) as SuspenseCollection | null
953
+ if (suspenseCollections && suspenseKey && instance.collection) {
954
+ const collection = instance.collection
985
955
  const removeCleanupListener = collection.on(`status:cleaned-up`, () =>
986
956
  releaseSuspenseCollection(
987
957
  suspenseCollections,
@@ -1013,13 +983,13 @@ function useLiveQueryImpl(
1013
983
  suspenseCollections.set(suspenseKey, entry)
1014
984
  }
1015
985
  }
1016
- configRef.current = configOrQueryOrCollection
1017
- depsRef.current = [...identityDeps]
986
+ instance.config = configOrQueryOrCollection
987
+ instance.deps = [...identityDeps]
1018
988
  }
1019
- clientRef.current = dbClient
1020
- queryHashRef.current = queryHash
1021
- suspenseKeyRef.current = suspenseKey
1022
- identityErrorRef.current = identityError
989
+ instance.client = dbClient
990
+ instance.queryHash = queryHash
991
+ instance.suspenseKey = suspenseKey
992
+ instance.identityError = identityError
1023
993
  }
1024
994
 
1025
995
  // Recreate the observer when the underlying collection changes. The observer
@@ -1035,38 +1005,38 @@ function useLiveQueryImpl(
1035
1005
  // hook's pre-observer loading policy, and — because wholesale delivers
1036
1006
  // nothing synchronously during subscribe — never notifies
1037
1007
  // useSyncExternalStore inside its own subscribe call.
1038
- observerRef.current = createLiveQueryObserver(collectionRef.current, {
1008
+ instance.observer = createLiveQueryObserver(instance.collection, {
1039
1009
  mode: `wholesale`,
1040
1010
  client: dbClient,
1041
- queryHash: queryHashRef.current,
1011
+ queryHash: instance.queryHash,
1042
1012
  onPreload: resumeDeferredCollections,
1043
1013
  })
1044
1014
  }
1045
- const observer = observerRef.current!
1015
+ const observer = instance.observer!
1046
1016
 
1047
1017
  // Stable subscribe bound to the current observer; the observer owns the
1048
1018
  // subscription, ready-race, and disposal.
1049
- const subscribeRef = useRef<
1050
- ((onStoreChange: () => void) => () => void) | null
1051
- >(null)
1052
- if (!subscribeRef.current || needsNewCollection) {
1053
- subscribeRef.current = (onStoreChange: () => void) => {
1054
- const unsubscribe = observer.subscribe(() => onStoreChange())
1019
+ if (!instance.subscribe || needsNewCollection) {
1020
+ instance.subscribe = (onStoreChange: () => void) => {
1021
+ const unsubscribe = observer.subscribe(onStoreChange)
1055
1022
  resumeDeferredCollections()
1056
1023
  return unsubscribe
1057
1024
  }
1058
1025
  }
1059
1026
 
1060
1027
  const returned = useSyncExternalStore(
1061
- subscribeRef.current,
1028
+ instance.subscribe,
1062
1029
  () => observer.getSnapshot(),
1063
1030
  () => observer.getServerSnapshot(),
1064
1031
  )
1065
- setLiveQueryResultInfo(returned, {
1066
- client: dbClient,
1067
- queryHash: queryHashRef.current,
1068
- identityError: identityErrorRef.current,
1069
- observer,
1070
- })
1032
+ // Only useLiveSuspenseQuery reads this, and it costs a define per render.
1033
+ if (forSuspense) {
1034
+ setLiveQueryResultInfo(returned, {
1035
+ client: dbClient,
1036
+ queryHash: instance.queryHash,
1037
+ identityError: instance.identityError,
1038
+ observer,
1039
+ })
1040
+ }
1071
1041
  return returned as any
1072
1042
  }