@tanstack/react-db 0.5.1 → 0.5.4

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.
@@ -324,6 +324,56 @@ See meta-framework/SKILL.md for full preloading patterns.
324
324
 
325
325
  ## Common Mistakes
326
326
 
327
+ ### CRITICAL Using === instead of eq() in .where()
328
+
329
+ Wrong — throws `InvalidWhereExpressionError` at runtime:
330
+
331
+ ```tsx
332
+ const { data } = useLiveQuery({
333
+ query: (q) =>
334
+ q
335
+ .from({ todo: todoCollection })
336
+ .where(({ todo }) => todo.completed === true),
337
+ })
338
+ ```
339
+
340
+ Correct — use expression functions from `@tanstack/react-db`:
341
+
342
+ ```tsx
343
+ import { useLiveQuery, eq } from '@tanstack/react-db'
344
+
345
+ const { data } = useLiveQuery({
346
+ query: (q) =>
347
+ q
348
+ .from({ todo: todoCollection })
349
+ .where(({ todo }) => eq(todo.completed, true)),
350
+ })
351
+ ```
352
+
353
+ JavaScript `===`, `!==`, `<`, and `>` evaluate immediately and return a boolean. Use expression functions such as `eq`, `gt`, `gte`, `lt`, `lte`, `and`, `or`, and `not` for structured `.where()` predicates. See db-core/live-queries/SKILL.md for the full operator reference.
354
+
355
+ ### CRITICAL Assigning useLiveQuery result directly instead of destructuring
356
+
357
+ Wrong — crashes with "map is not a function":
358
+
359
+ ```tsx
360
+ const todos = useLiveQuery({
361
+ query: (q) => q.from({ todo: todoCollection }),
362
+ })
363
+ return todos.map((t) => <li>{t.text}</li>) // TypeError: todos.map is not a function
364
+ ```
365
+
366
+ Correct — destructure `{ data }`:
367
+
368
+ ```tsx
369
+ const { data: todos } = useLiveQuery({
370
+ query: (q) => q.from({ todo: todoCollection }),
371
+ })
372
+ return todos.map((t) => <li key={t.id}>{t.text}</li>)
373
+ ```
374
+
375
+ `useLiveQuery` returns an object with `data`, `isLoading`, `status`, and other fields. For queries that can be disabled, use `{ data: todos = [] }` to handle `data` being `undefined`.
376
+
327
377
  ### CRITICAL Using opaque query logic without queryKey
328
378
 
329
379
  Wrong:
@@ -0,0 +1,15 @@
1
+ // Bundlers inline `process.env.NODE_ENV` even where `process` does not exist,
2
+ // so it is read directly; without either, warnings stay off.
3
+ export function shouldWarnInDevelopment(disableEnvVar: string): boolean {
4
+ try {
5
+ if (process.env.NODE_ENV === `production`) return false
6
+ } catch {
7
+ return false
8
+ }
9
+ // A page may supply a `process` shim without `env`; nothing disabled it.
10
+ try {
11
+ return process.env[disableEnvVar] !== `1`
12
+ } catch {
13
+ return true
14
+ }
15
+ }
@@ -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,
@@ -55,22 +56,20 @@ const suspenseCollectionsByClient = new WeakMap<
55
56
  DbClient,
56
57
  Map<string, SuspenseCollectionEntry>
57
58
  >()
58
- const suspenseSourceIds = new WeakMap<object, number>()
59
- let nextSuspenseSourceId = 0
59
+ const sourceObjectTokens = new WeakMap<object, number>()
60
+ let nextSourceObjectToken = 0
60
61
 
61
- function getSuspenseSourceId(source: object): number {
62
- let id = suspenseSourceIds.get(source)
62
+ function getSourceObjectToken(source: object): number {
63
+ let id = sourceObjectTokens.get(source)
63
64
  if (id === undefined) {
64
- id = ++nextSuspenseSourceId
65
- suspenseSourceIds.set(source, id)
65
+ id = ++nextSourceObjectToken
66
+ sourceObjectTokens.set(source, id)
66
67
  }
67
68
  return id
68
69
  }
69
70
 
70
- function getUnscopedSuspenseKey(
71
- preparedValue: unknown,
72
- queryHash: string,
73
- ): string {
71
+ function getPreparedSources(preparedValue: unknown): Array<{ id: string }> {
72
+ if (isCollection(preparedValue)) return [preparedValue]
74
73
  const query =
75
74
  preparedValue instanceof BaseQueryBuilder
76
75
  ? preparedValue
@@ -80,10 +79,19 @@ function getUnscopedSuspenseKey(
80
79
  preparedValue.query instanceof BaseQueryBuilder
81
80
  ? preparedValue.query
82
81
  : undefined
83
- if (!query) return queryHash
84
- const sourceIds = IR.collectCollectionSources(query._getQuery()).map(
85
- ({ collection }) => getSuspenseSourceId(collection),
86
- )
82
+ return query
83
+ ? IR.collectCollectionSources(query._getQuery()).map(
84
+ ({ collection }) => collection,
85
+ )
86
+ : []
87
+ }
88
+
89
+ function getSourceQualifiedSuspenseKey(
90
+ preparedValue: unknown,
91
+ queryHash: string,
92
+ ): string {
93
+ const sourceIds = getPreparedSources(preparedValue).map(getSourceObjectToken)
94
+ if (sourceIds.length === 0) return queryHash
87
95
  return `${sourceIds.join(`,`)}:${queryHash}`
88
96
  }
89
97
 
@@ -172,16 +180,6 @@ export function warnDeprecatedDepsArray(
172
180
  )
173
181
  }
174
182
 
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
183
  function getCurrentTime(): number {
186
184
  return typeof performance !== `undefined` &&
187
185
  typeof performance.now === `function`
@@ -336,40 +334,6 @@ export function warnUnhashableDerivedIdentity(
336
334
  )
337
335
  }
338
336
 
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
337
  /**
374
338
  * Create a live query using a query function.
375
339
  * @param queryFn - Query function that defines what data to fetch
@@ -547,9 +511,7 @@ export function useLiveQuery<
547
511
  deps?: Array<unknown>,
548
512
  ): {
549
513
  state:
550
- | Map<string | number, GetResult<TContext>>
551
- | Map<TKey, TResult>
552
- | undefined
514
+ Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined
553
515
  data: InferResultType<TContext> | Array<TResult> | undefined
554
516
  collection:
555
517
  | Collection<GetResult<TContext>, string | number, {}>
@@ -771,6 +733,36 @@ export function useLiveQueryForSuspense(
771
733
  return useLiveQueryImpl(configOrQueryOrCollection, deps, true)
772
734
  }
773
735
 
736
+ // What one hook instance keeps across renders. It lives in a single ref
737
+ // slot, so a render neither looks up nor allocates more.
738
+ function createHookInstance(dbClient: DbClient | undefined) {
739
+ return {
740
+ collection: null as Collection<object, string | number, {}> | null,
741
+ deps: null as Array<unknown> | null,
742
+ config: null as unknown,
743
+ client: dbClient,
744
+ legacyUnhashableIdentity: [`legacy-unhashable`] as Array<unknown>,
745
+ derivedIdentityProfiler: {
746
+ renderCount: 0,
747
+ totalMs: 0,
748
+ maxMs: 0,
749
+ warned: false,
750
+ } as DerivedIdentityProfiler,
751
+ deferredCollections: new Set<
752
+ CollectionImpl<any, string | number, any, any, any>
753
+ >(),
754
+ sourceIds: {
755
+ unscoped: new Map<string, number>(),
756
+ byClient: new WeakMap<DbClient, Map<string, number>>(),
757
+ },
758
+ observer: null as LiveQueryObserver<object, string | number> | null,
759
+ queryHash: undefined as string | undefined,
760
+ suspenseKey: undefined as string | undefined,
761
+ identityError: undefined as UnhashableQueryIRError | undefined,
762
+ subscribe: null as ((onStoreChange: () => void) => () => void) | null,
763
+ }
764
+ }
765
+
774
766
  function useLiveQueryImpl(
775
767
  configOrQueryOrCollection: any,
776
768
  deps: Array<unknown> | undefined,
@@ -784,32 +776,21 @@ function useLiveQueryImpl(
784
776
  : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)
785
777
  const resolvedDeps = deps ?? []
786
778
 
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)
779
+ const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)
780
+ const instance = (instanceRef.current ??= createHookInstance(dbClient))
781
+ const resumeDeferredCollections = () => {
782
+ const collections = Array.from(instance.deferredCollections)
783
+ instance.deferredCollections.clear()
784
+ const errors: Array<unknown> = []
785
+ for (const collection of collections) {
786
+ try {
787
+ collection._resumeSyncStart()
788
+ } catch (error) {
789
+ errors.push(error)
790
+ }
791
+ }
792
+ if (errors.length) throw errors[0]
793
+ }
813
794
 
814
795
  const queryKey = !inputIsCollection
815
796
  ? getExplicitQueryKey(configOrQueryOrCollection)
@@ -825,22 +806,29 @@ function useLiveQueryImpl(
825
806
  streamIdentity = [`queryKey`, queryKey]
826
807
  } else if (deps !== undefined) {
827
808
  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
- }
809
+ // Deps decide reuse. Only hydration and Suspense read the query hash; the
810
+ // development warning about unhashable queries still derives it.
811
+ if (
812
+ dbClient ||
813
+ forSuspense ||
814
+ shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)
815
+ )
816
+ try {
817
+ preparedQueryValue = prepareQueryValue(
818
+ configOrQueryOrCollection,
819
+ dbClient,
820
+ instance.deferredCollections,
821
+ )
822
+ streamIdentity = [
823
+ `deps`,
824
+ resolvedDeps,
825
+ getPreparedLiveQueryIdentity(preparedQueryValue),
826
+ ]
827
+ } catch (error) {
828
+ if (!(error instanceof UnhashableQueryIRError)) throw error
829
+ warnUnhashableDerivedIdentity(error)
830
+ identityError = error
831
+ }
844
832
  } else if (inputIsCollection) {
845
833
  identityDeps = []
846
834
  streamIdentity = [`collection`, configOrQueryOrCollection.id]
@@ -848,8 +836,8 @@ function useLiveQueryImpl(
848
836
  const preparation = prepareDerivedQuery(
849
837
  configOrQueryOrCollection,
850
838
  dbClient,
851
- derivedIdentityProfilerRef.current,
852
- deferredCollectionsRef.current,
839
+ instance.derivedIdentityProfiler,
840
+ instance.deferredCollections,
853
841
  )
854
842
  preparedQueryValue = preparation.value
855
843
  if (preparation.status === `hashable`) {
@@ -857,7 +845,7 @@ function useLiveQueryImpl(
857
845
  streamIdentity = preparation.identityDeps
858
846
  } else {
859
847
  warnUnhashableDerivedIdentity(preparation.error)
860
- identityDeps = legacyUnhashableIdentityRef.current
848
+ identityDeps = instance.legacyUnhashableIdentity
861
849
  identityError = preparation.error
862
850
  }
863
851
  }
@@ -880,36 +868,60 @@ function useLiveQueryImpl(
880
868
  warnDeprecatedDepsArray()
881
869
  }
882
870
 
871
+ if (
872
+ queryKey === undefined &&
873
+ deps === undefined &&
874
+ preparedQueryValue !== unpreparedQueryValue
875
+ ) {
876
+ const prior = dbClient
877
+ ? instance.sourceIds.byClient.get(dbClient)
878
+ : instance.sourceIds.unscoped
879
+ const seen = new Map<string, number>()
880
+ for (const source of getPreparedSources(preparedQueryValue)) {
881
+ const token = getSourceObjectToken(source)
882
+ const previous = seen.get(source.id) ?? prior?.get(source.id)
883
+ if (previous !== undefined && previous !== token) {
884
+ // The rejected render must not retain a shared Collection's sync deferral.
885
+ resumeDeferredCollections()
886
+ throw new Error(
887
+ `[useLiveQuery] Source Collection "${source.id}" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`,
888
+ )
889
+ }
890
+ seen.set(source.id, token)
891
+ }
892
+ const bindings = prior ?? new Map<string, number>()
893
+ for (const [id, token] of seen) bindings.set(id, token)
894
+ if (dbClient) instance.sourceIds.byClient.set(dbClient, bindings)
895
+ }
896
+
883
897
  const canReuseSuspenseKey =
884
898
  forSuspense &&
885
899
  !inputIsCollection &&
886
900
  queryHash !== undefined &&
887
- !dbClient &&
888
- collectionRef.current !== null &&
889
- clientRef.current === dbClient &&
890
- queryHashRef.current === queryHash &&
891
- suspenseKeyRef.current !== undefined
901
+ instance.collection !== null &&
902
+ instance.client === dbClient &&
903
+ instance.queryHash === queryHash &&
904
+ instance.suspenseKey !== undefined
892
905
 
893
906
  if (
894
907
  forSuspense &&
895
908
  !inputIsCollection &&
896
909
  queryHash &&
897
- !dbClient &&
898
910
  !canReuseSuspenseKey &&
899
911
  preparedQueryValue === unpreparedQueryValue
900
912
  ) {
901
913
  preparedQueryValue = prepareQueryValue(
902
914
  configOrQueryOrCollection,
903
915
  dbClient,
904
- deferredCollectionsRef.current,
916
+ instance.deferredCollections,
905
917
  )
906
918
  }
907
919
 
908
920
  const suspenseKey =
909
- queryHash && !dbClient
921
+ queryHash && forSuspense && !inputIsCollection
910
922
  ? canReuseSuspenseKey
911
- ? suspenseKeyRef.current
912
- : getUnscopedSuspenseKey(preparedQueryValue, queryHash)
923
+ ? instance.suspenseKey
924
+ : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash)
913
925
  : queryHash
914
926
 
915
927
  const suspenseCollections =
@@ -922,24 +934,17 @@ function useLiveQueryImpl(
922
934
  const suspenseCollection = suspenseEntry?.collection
923
935
 
924
936
  const identityChanged =
925
- depsRef.current === null ||
937
+ instance.deps === null ||
926
938
  (deps !== undefined
927
- ? depsRef.current.length !== identityDeps.length ||
928
- depsRef.current.some((dep, index) => dep !== identityDeps[index])
929
- : !deepEquals(depsRef.current, identityDeps))
939
+ ? instance.deps.length !== identityDeps.length ||
940
+ instance.deps.some((dep, index) => dep !== identityDeps[index])
941
+ : !deepEquals(instance.deps, identityDeps))
930
942
 
931
943
  // Check if we need to create/recreate the collection
932
944
  const needsNewCollection =
933
- !collectionRef.current ||
934
- (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||
935
- (!inputIsCollection && (clientRef.current !== dbClient || identityChanged))
936
-
937
- const resumeDeferredCollections = () => {
938
- for (const collection of deferredCollectionsRef.current) {
939
- collection._resumeSyncStart()
940
- }
941
- deferredCollectionsRef.current.clear()
942
- }
945
+ !instance.collection ||
946
+ (inputIsCollection && instance.config !== configOrQueryOrCollection) ||
947
+ (!inputIsCollection && (instance.client !== dbClient || identityChanged))
943
948
 
944
949
  if (needsNewCollection) {
945
950
  if (inputIsCollection) {
@@ -963,25 +968,28 @@ function useLiveQueryImpl(
963
968
  }
964
969
  // It's already a collection, ensure sync is started for React hooks
965
970
  configOrQueryOrCollection.startSyncImmediate()
966
- collectionRef.current = configOrQueryOrCollection
967
- configRef.current = configOrQueryOrCollection
971
+ instance.collection = configOrQueryOrCollection
972
+ instance.config = configOrQueryOrCollection
968
973
  } else {
969
974
  if (suspenseCollection) {
970
- collectionRef.current = suspenseCollection
975
+ instance.collection = suspenseCollection
971
976
  } else {
972
977
  if (preparedQueryValue === unpreparedQueryValue) {
973
978
  preparedQueryValue = prepareQueryValue(
974
979
  configOrQueryOrCollection,
975
980
  dbClient,
976
- deferredCollectionsRef.current,
981
+ instance.deferredCollections,
977
982
  )
978
983
  }
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
984
+ instance.collection = resolveLiveQueryValue(preparedQueryValue, {
985
+ gcTime: forSuspense
986
+ ? DEFAULT_SUSPENSE_GC_TIME_MS
987
+ : DEFAULT_GC_TIME_MS,
988
+ // Hydration and Suspense key the live-query Collection by identity.
989
+ pool: !forSuspense && !dbClient,
990
+ }) as SuspenseCollection | null
991
+ if (suspenseCollections && suspenseKey && instance.collection) {
992
+ const collection = instance.collection
985
993
  const removeCleanupListener = collection.on(`status:cleaned-up`, () =>
986
994
  releaseSuspenseCollection(
987
995
  suspenseCollections,
@@ -1013,13 +1021,13 @@ function useLiveQueryImpl(
1013
1021
  suspenseCollections.set(suspenseKey, entry)
1014
1022
  }
1015
1023
  }
1016
- configRef.current = configOrQueryOrCollection
1017
- depsRef.current = [...identityDeps]
1024
+ instance.config = configOrQueryOrCollection
1025
+ instance.deps = [...identityDeps]
1018
1026
  }
1019
- clientRef.current = dbClient
1020
- queryHashRef.current = queryHash
1021
- suspenseKeyRef.current = suspenseKey
1022
- identityErrorRef.current = identityError
1027
+ instance.client = dbClient
1028
+ instance.queryHash = queryHash
1029
+ instance.suspenseKey = suspenseKey
1030
+ instance.identityError = identityError
1023
1031
  }
1024
1032
 
1025
1033
  // Recreate the observer when the underlying collection changes. The observer
@@ -1035,38 +1043,38 @@ function useLiveQueryImpl(
1035
1043
  // hook's pre-observer loading policy, and — because wholesale delivers
1036
1044
  // nothing synchronously during subscribe — never notifies
1037
1045
  // useSyncExternalStore inside its own subscribe call.
1038
- observerRef.current = createLiveQueryObserver(collectionRef.current, {
1046
+ instance.observer = createLiveQueryObserver(instance.collection, {
1039
1047
  mode: `wholesale`,
1040
1048
  client: dbClient,
1041
- queryHash: queryHashRef.current,
1049
+ queryHash: instance.queryHash,
1042
1050
  onPreload: resumeDeferredCollections,
1043
1051
  })
1044
1052
  }
1045
- const observer = observerRef.current!
1053
+ const observer = instance.observer!
1046
1054
 
1047
1055
  // Stable subscribe bound to the current observer; the observer owns the
1048
1056
  // 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())
1057
+ if (!instance.subscribe || needsNewCollection) {
1058
+ instance.subscribe = (onStoreChange: () => void) => {
1059
+ const unsubscribe = observer.subscribe(onStoreChange)
1055
1060
  resumeDeferredCollections()
1056
1061
  return unsubscribe
1057
1062
  }
1058
1063
  }
1059
1064
 
1060
1065
  const returned = useSyncExternalStore(
1061
- subscribeRef.current,
1066
+ instance.subscribe,
1062
1067
  () => observer.getSnapshot(),
1063
1068
  () => observer.getServerSnapshot(),
1064
1069
  )
1065
- setLiveQueryResultInfo(returned, {
1066
- client: dbClient,
1067
- queryHash: queryHashRef.current,
1068
- identityError: identityErrorRef.current,
1069
- observer,
1070
- })
1070
+ // Only useLiveSuspenseQuery reads this, and it costs a define per render.
1071
+ if (forSuspense) {
1072
+ setLiveQueryResultInfo(returned, {
1073
+ client: dbClient,
1074
+ queryHash: instance.queryHash,
1075
+ identityError: instance.identityError,
1076
+ observer,
1077
+ })
1078
+ }
1071
1079
  return returned as any
1072
1080
  }