@qiaopeng/tanstack-query-plus 0.2.3 → 0.2.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.
package/README.md CHANGED
@@ -968,7 +968,7 @@ const list7 = conditionalUpdateItems(
968
968
  )
969
969
  ```
970
970
 
971
- ### 7.9 完整示例:Todo 应用
971
+ ### 7.9 完整示例:Todo 应用
972
972
 
973
973
  ```tsx
974
974
  import { useEnhancedQuery, useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
@@ -1038,54 +1038,55 @@ function TodoApp() {
1038
1038
  </ul>
1039
1039
  </div>
1040
1040
  )
1041
+ }
1041
1042
  }
1042
1043
  ```
1043
1044
 
1044
- ### 7.10 跨分页一致性:前缀失效与缓存对齐
1045
+ ### 7.10 分页家族一致性(避免分页切换回退)
1045
1046
 
1046
- 在分页列表中,乐观更新通常只更新“当前页”的缓存。为了保证切换到其他页时也看到最新数据,可以使用前缀级失效与跨页缓存对齐:
1047
+ 在带分页/筛选/排序的列表中,编辑、新增、删除、状态变更成功后切换 `page/pageSize` 时,可能命中同一资源的另一查询变体,从而短暂显示旧快照。本库提供可选的“家族一致性”能力,保障在成功后切换分页不回退。
1047
1048
 
1048
- 1. 使用统一的域前缀管理 Query Key(例如 `products` 域):
1049
-
1050
- ```tsx
1051
- import { createDomainKeyFactory } from '@qiaopeng/tanstack-query-plus/core'
1052
-
1053
- const productKeys = createDomainKeyFactory('products')
1054
- const listPrefix = productKeys.lists() // ['tanstack-query', 'products', 'list']
1055
- ```
1056
-
1057
- 2. 在 mutation 的乐观更新中启用跨页对齐与前缀失效:
1049
+ - 开启方式:在 `useMutation` 传入 `consistency` 配置(默认关闭,显式启用)
1050
+ - 安全默认:`mode: 'invalidate'` 只执行家族失效,确保最终与服务端一致
1051
+ - 进阶模式:`mode: 'sync+invalidate'` 先对缓存中已存在的变体按 `id` 合并更新,再统一失效
1052
+ - 形状适配:通过 `listSelector` 适配 `{items,total}` 结构;无法识别时自动降级为仅失效
1058
1053
 
1059
1054
  ```tsx
1060
1055
  import { useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
1056
+ import { createPaginatedKey } from '@qiaopeng/tanstack-query-plus/core'
1061
1057
 
1062
- function UpdateProduct({ product }) {
1063
- const mutation = useMutation({
1064
- mutationFn: (patch) => api.updateProduct(product.id, patch),
1058
+ function useUpdateProduct({ page, pageSize }) {
1059
+ return useMutation({
1060
+ mutationFn: (updated) => api.updateProduct(updated.id, updated),
1061
+
1062
+ // 当前页的乐观更新:先更新 UI,再发请求,失败自动回滚
1065
1063
  optimistic: {
1066
- queryKey: productKeys.detail(product.id),
1067
- updater: (oldData, patch) => ({ ...oldData, ...patch }),
1068
- invalidateScope: 'prefix',
1069
- invalidatePrefixKey: listPrefix,
1070
- reconcileCachedPages: true
1071
- }
1072
- })
1064
+ queryKey: createPaginatedKey(['products', 'list'], page, pageSize),
1065
+ updater: (old, updated) => old?.map((p) => (p.id === updated.id ? { ...p, ...updated } : p)),
1066
+ },
1073
1067
 
1074
- return (
1075
- <button onClick={() => mutation.mutate({ title: '新标题' })}>
1076
- 更新商品
1077
- </button>
1078
- )
1068
+ // 家族一致性:编辑成功后,保障跨分页/筛选/排序的变体不回退
1069
+ consistency: {
1070
+ baseKey: ['products', 'list'],
1071
+ mode: 'sync+invalidate',
1072
+ idField: 'id',
1073
+ // 适配分页对象:提取 items;不确定时返回 null 将仅失效
1074
+ listSelector: (data) => {
1075
+ if (data && typeof data === 'object' && 'items' in (data as any)) {
1076
+ return { items: (data as any).items, total: (data as any).total }
1077
+ }
1078
+ if (Array.isArray(data)) return { items: data }
1079
+ return null
1080
+ },
1081
+ maxKeys: 50,
1082
+ },
1083
+ })
1079
1084
  }
1080
1085
  ```
1081
1086
 
1082
- 行为说明:
1083
- - 乐观阶段:先更新当前 key,对已缓存的所有分页列表也应用同样的更新,使切页立即看到新值。
1084
- - 成功后:对域前缀进行失效,后续展示从服务端获取最终一致数据。
1085
-
1086
- 注意事项:
1087
- - 请确保分页查询的 Key 都共享同一前缀(例如通过 `createDomainKeyFactory`),否则前缀失效无法覆盖所有分页。
1088
- - 在超大列表场景中,跨页对齐可能带来一定开销,可按需开启。
1087
+ 适用操作与行为说明:
1088
+ - 编辑/删除:在 `sync+invalidate` 模式下,会对已缓存的家族变体按 `id` 合并或移除;随后统一失效,最终以服务端为准
1089
+ - 新增/状态变更:默认不做跨页注入,仅当前页处理并家族失效;需要跨页放置时请在服务端裁决归属
1089
1090
 
1090
1091
  现在你已经掌握了数据变更和乐观更新。接下来,让我们学习如何处理无限滚动和分页场景。
1091
1092
 
@@ -3245,6 +3246,36 @@ function SearchResults({ query }) {
3245
3246
  }
3246
3247
  ```
3247
3248
 
3249
+ ### 14.11 家族一致性工具
3250
+
3251
+ 在某些高级场景下,你可能需要自行枚举并同步同一资源的家族查询变体(分页/筛选/排序等)。本库提供了工具函数用于匹配与安全同步:
3252
+
3253
+ ```tsx
3254
+ import { useQueryClient } from '@qiaopeng/tanstack-query-plus'
3255
+ import { findFamilyQueries, syncEntityAcrossFamily } from '@qiaopeng/tanstack-query-plus/utils'
3256
+
3257
+ function useManualFamilySync() {
3258
+ const queryClient = useQueryClient()
3259
+ const sync = (updated) => {
3260
+ const keys = findFamilyQueries(queryClient, { baseKey: ['products', 'list'], maxKeys: 50 })
3261
+ syncEntityAcrossFamily(queryClient, keys, updated, {
3262
+ idField: 'id',
3263
+ listSelector: (data) => {
3264
+ if (data && typeof data === 'object' && 'items' in (data as any)) {
3265
+ return { items: (data as any).items, total: (data as any).total }
3266
+ }
3267
+ if (Array.isArray(data)) return { items: data }
3268
+ return null
3269
+ },
3270
+ })
3271
+ keys.forEach((key) => queryClient.invalidateQueries({ queryKey: key }))
3272
+ }
3273
+ return { sync }
3274
+ }
3275
+ ```
3276
+
3277
+ 提示:上述工具已在 `useMutation` 的一致性配置中自动使用;仅在需要手动控制时使用它们。
3278
+
3248
3279
  现在你已经掌握了所有核心功能!最后,让我们看看一些最佳实践和常见问题。
3249
3280
 
3250
3281
  ---
@@ -1 +1 @@
1
- {"version":3,"file":"useMutation.d.ts","sourceRoot":"","sources":["../../src/hooks/useMutation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useMutation.d.ts","sourceRoot":"","sources":["../../src/hooks/useMutation.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { findFamilyQueries, syncEntityAcrossFamily } from "../utils/consistency.js";
1
2
  import { useQueryClient, useMutation as useTanStackMutation } from "@tanstack/react-query";
2
3
  import { DEFAULT_MUTATION_CONFIG } from "../core/config.js";
3
4
  export function useMutation(options) {
@@ -44,14 +45,7 @@ export function useMutation(options) {
44
45
  }
45
46
  });
46
47
  }
47
- const safeUpdater = (oldData) => {
48
- const next = optimistic.updater(oldData, mappedVariables);
49
- return typeof next === "undefined" ? oldData : next;
50
- };
51
- queryClient.setQueryData(optimistic.queryKey, safeUpdater);
52
- if (optimistic.reconcileCachedPages && optimistic.invalidatePrefixKey) {
53
- queryClient.setQueriesData({ queryKey: optimistic.invalidatePrefixKey }, safeUpdater);
54
- }
48
+ queryClient.setQueryData(optimistic.queryKey, (oldData) => optimistic.updater(oldData, mappedVariables));
55
49
  const mutateCallback = onMutate;
56
50
  const userContext = onMutate ? await mutateCallback(variables) : undefined;
57
51
  return { previousData, userContext };
@@ -76,8 +70,21 @@ export function useMutation(options) {
76
70
  }
77
71
  };
78
72
  mutationConfig.onSuccess = (data, variables, context) => {
79
- const scope = optimistic.invalidateScope === "prefix" && optimistic.invalidatePrefixKey ? optimistic.invalidatePrefixKey : optimistic.queryKey;
80
- queryClient.invalidateQueries({ queryKey: scope });
73
+ queryClient.invalidateQueries({ queryKey: optimistic.queryKey });
74
+ const consistency = options.consistency;
75
+ if (consistency && Array.isArray(consistency.baseKey) && consistency.baseKey.length > 0) {
76
+ try {
77
+ const keys = findFamilyQueries(queryClient, { baseKey: consistency.baseKey, maxKeys: consistency.maxKeys });
78
+ if (consistency.mode === "sync+invalidate") {
79
+ const updatedEntity = (typeof data === "object" && data !== null) ? data : (typeof variables === "object" && variables !== null ? variables : undefined);
80
+ if (updatedEntity) {
81
+ syncEntityAcrossFamily(queryClient, keys, updatedEntity, { idField: consistency.idField, listSelector: consistency.listSelector });
82
+ }
83
+ }
84
+ keys.forEach((key) => { queryClient.invalidateQueries({ queryKey: key }); });
85
+ }
86
+ catch { }
87
+ }
81
88
  if (onSuccess) {
82
89
  const successCallback = onSuccess;
83
90
  successCallback(data, variables, context?.userContext);
@@ -0,0 +1,13 @@
1
+ import type { QueryKey } from "@tanstack/react-query";
2
+ export type ConsistencyMode = "invalidate" | "sync+invalidate";
3
+ export interface FamilyConsistencyOptions {
4
+ baseKey: QueryKey;
5
+ mode?: ConsistencyMode;
6
+ idField?: string;
7
+ listSelector?: (data: unknown) => unknown[] | {
8
+ items: unknown[];
9
+ total?: number;
10
+ } | null;
11
+ maxKeys?: number;
12
+ }
13
+ //# sourceMappingURL=consistency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consistency.d.ts","sourceRoot":"","sources":["../../src/types/consistency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,iBAAiB,CAAC;AAC/D,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,QAAQ,CAAC;IAClB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,EAAE,GAAG;QAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -6,6 +6,7 @@ export * from "./optimistic.js";
6
6
  export * from "./persistence.js";
7
7
  export * from "./selectors.js";
8
8
  export * from "./suspense.js";
9
+ export * from "./consistency.js";
9
10
  export interface MutationContext<TData = unknown, TContext = unknown> {
10
11
  previousData?: TData;
11
12
  userContext?: TContext;
@@ -18,9 +19,16 @@ export interface MutationOptions<TData, TError, TVariables, TContext = unknown>
18
19
  enabled?: boolean;
19
20
  fieldMapping?: Record<string, string>;
20
21
  rollback?: <TQueryData = unknown>(previousData: TQueryData, error: Error) => void;
21
- invalidateScope?: "exact" | "prefix";
22
- invalidatePrefixKey?: QueryKey;
23
- reconcileCachedPages?: boolean;
22
+ };
23
+ consistency?: {
24
+ baseKey: QueryKey;
25
+ mode?: "invalidate" | "sync+invalidate";
26
+ idField?: string;
27
+ listSelector?: (data: unknown) => unknown[] | {
28
+ items: unknown[];
29
+ total?: number;
30
+ } | null;
31
+ maxKeys?: number;
24
32
  };
25
33
  }
26
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC1E,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,MAAM,WAAW,eAAe,CAAC,KAAK,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO;IAAI,YAAY,CAAC,EAAE,KAAK,CAAC;IAAC,WAAW,CAAC,EAAE,QAAQ,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE;AAC9I,MAAM,WAAW,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,GAAG,OAAO,CAAE,SAAQ,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC;IAC7I,UAAU,CAAC,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,EAAE,UAAU,GAAG,SAAS,EAAE,SAAS,EAAE,UAAU,KAAK,UAAU,GAAG,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,GAAG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;QAAC,mBAAmB,CAAC,EAAE,QAAQ,CAAC;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3Y"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC1E,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,MAAM,WAAW,eAAe,CAAC,KAAK,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO;IAAI,YAAY,CAAC,EAAE,KAAK,CAAC;IAAC,WAAW,CAAC,EAAE,QAAQ,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE;AAC9I,MAAM,WAAW,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,GAAG,OAAO,CAAE,SAAQ,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC;IAC7I,UAAU,CAAC,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,EAAE,UAAU,GAAG,SAAS,EAAE,SAAS,EAAE,UAAU,KAAK,UAAU,GAAG,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,GAAG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;KAAE,CAAC;IACpS,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,QAAQ,CAAC;QAAC,IAAI,CAAC,EAAE,YAAY,GAAG,iBAAiB,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,EAAE,GAAG;YAAE,KAAK,EAAE,OAAO,EAAE,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7M"}
@@ -5,3 +5,4 @@ export * from "./optimistic.js";
5
5
  export * from "./persistence.js";
6
6
  export * from "./selectors.js";
7
7
  export * from "./suspense.js";
8
+ export * from "./consistency.js";
@@ -0,0 +1,14 @@
1
+ import type { QueryClient, QueryKey } from "@tanstack/react-query";
2
+ export interface FamilyQueryMatchOptions {
3
+ baseKey: QueryKey;
4
+ maxKeys?: number;
5
+ }
6
+ export declare function findFamilyQueries(queryClient: QueryClient, options: FamilyQueryMatchOptions): QueryKey[];
7
+ export declare function syncEntityAcrossFamily(queryClient: QueryClient, keys: QueryKey[], updatedEntity: unknown, options: {
8
+ idField?: string;
9
+ listSelector?: (data: unknown) => unknown[] | {
10
+ items: unknown[];
11
+ total?: number;
12
+ } | null;
13
+ }): void;
14
+ //# sourceMappingURL=consistency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consistency.d.ts","sourceRoot":"","sources":["../../src/utils/consistency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEnE,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,QAAQ,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,uBAAuB,GAAG,QAAQ,EAAE,CAQxG;AAeD,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,EAAE,GAAG;QAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CA6BzO"}
@@ -0,0 +1,68 @@
1
+ function isObjectEqual(a, b) {
2
+ if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
3
+ return JSON.stringify(a) === JSON.stringify(b);
4
+ }
5
+ return a === b;
6
+ }
7
+ function startsWithKey(key, prefix) {
8
+ if (prefix.length > key.length)
9
+ return false;
10
+ for (let i = 0; i < prefix.length; i++) {
11
+ if (!isObjectEqual(key[i], prefix[i]))
12
+ return false;
13
+ }
14
+ return true;
15
+ }
16
+ export function findFamilyQueries(queryClient, options) {
17
+ const { baseKey, maxKeys } = options;
18
+ const queries = queryClient.getQueryCache().findAll({ predicate: (q) => startsWithKey(q.queryKey, baseKey) });
19
+ const keys = queries.map((q) => q.queryKey);
20
+ if (typeof maxKeys === "number" && maxKeys > 0) {
21
+ return keys.slice(0, maxKeys);
22
+ }
23
+ return keys;
24
+ }
25
+ function updateArrayById(items, updated, idField) {
26
+ const list = Array.isArray(items) ? items : [];
27
+ const id = updated[idField];
28
+ if (id === undefined)
29
+ return list;
30
+ return list.map((item) => {
31
+ const currentId = item[idField];
32
+ if (currentId === id) {
33
+ return { ...item, ...updated };
34
+ }
35
+ return item;
36
+ });
37
+ }
38
+ export function syncEntityAcrossFamily(queryClient, keys, updatedEntity, options) {
39
+ const idField = options.idField ?? "id";
40
+ keys.forEach((key) => {
41
+ const oldData = queryClient.getQueryData(key);
42
+ if (Array.isArray(oldData)) {
43
+ const next = updateArrayById(oldData, updatedEntity, idField);
44
+ queryClient.setQueryData(key, next);
45
+ return;
46
+ }
47
+ if (oldData && typeof oldData === "object") {
48
+ const selector = options.listSelector;
49
+ if (selector) {
50
+ const selected = selector(oldData);
51
+ if (selected === null)
52
+ return;
53
+ if (selected && Array.isArray(selected.items)) {
54
+ const nextItems = updateArrayById(selected.items, updatedEntity, idField);
55
+ const next = { ...oldData, items: nextItems };
56
+ queryClient.setQueryData(key, next);
57
+ return;
58
+ }
59
+ }
60
+ if (Object.prototype.hasOwnProperty.call(oldData, "items")) {
61
+ const items = oldData["items"];
62
+ const nextItems = updateArrayById(items, updatedEntity, idField);
63
+ const next = { ...oldData, items: nextItems };
64
+ queryClient.setQueryData(key, next);
65
+ }
66
+ }
67
+ });
68
+ }
@@ -6,4 +6,5 @@ export { getPrefetchManager, type InteractionRecord, type NetworkSpeed, type Pre
6
6
  export { createQueryKeyFactory, createSimpleQueryKeyFactory, extractParamsFromKey, isQueryKeyEqual, type NormalizeConfig, normalizeQueryParams, type QueryKeyFactory, type QueryKeyFactoryConfig } from "./queryKey.js";
7
7
  export { compose, selectById, selectByIds, selectCount, selectField, selectFields, selectFirst, selectItems, selectLast, selectMap, selectors, selectTotal, selectWhere } from "./selectors.js";
8
8
  export { deepClone, formatBytes, getStorageUsage, isStorageAvailable } from "./storage.js";
9
+ export { findFamilyQueries, syncEntityAcrossFamily, type FamilyQueryMatchOptions } from "./consistency.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACpH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAC3H,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,KAAK,sBAAsB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC1P,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpO,OAAO,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,oBAAoB,EAAE,KAAK,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACxN,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAChM,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACpH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAC3H,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,KAAK,sBAAsB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC1P,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpO,OAAO,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,oBAAoB,EAAE,KAAK,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACxN,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAChM,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,KAAK,uBAAuB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -6,3 +6,4 @@ export { getPrefetchManager, resetPrefetchManager, SmartPrefetchManager } from "
6
6
  export { createQueryKeyFactory, createSimpleQueryKeyFactory, extractParamsFromKey, isQueryKeyEqual, normalizeQueryParams } from "./queryKey.js";
7
7
  export { compose, selectById, selectByIds, selectCount, selectField, selectFields, selectFirst, selectItems, selectLast, selectMap, selectors, selectTotal, selectWhere } from "./selectors.js";
8
8
  export { deepClone, formatBytes, getStorageUsage, isStorageAvailable } from "./storage.js";
9
+ export { findFamilyQueries, syncEntityAcrossFamily } from "./consistency.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qiaopeng/tanstack-query-plus",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Enhanced TanStack Query toolkit: defaults, hooks, persistence, offline, utils",
5
5
  "author": "qiaopeng",
6
6
  "license": "MIT",