@qiaopeng/tanstack-query-plus 0.4.3 → 0.5.0

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
@@ -1042,55 +1042,6 @@ function TodoApp() {
1042
1042
  }
1043
1043
  ```
1044
1044
 
1045
- ### 7.10 分页家族一致性(避免分页切换回退)
1046
-
1047
- 在带分页/筛选/排序的列表中,编辑、新增、删除、状态变更成功后切换 `page/pageSize` 时,可能命中同一资源的另一查询变体,从而短暂显示旧快照。本库提供可选的“家族一致性”能力,保障在成功后切换分页不回退。
1048
-
1049
- - 开启方式:在 `useMutation` 传入 `consistency` 配置
1050
- - 默认策略:`mode: 'sync+invalidate'` 先同步更新缓存,再延迟失效,确保 UI 立即响应且最终一致
1051
- - 安全策略:`mode: 'invalidate-only'` 仅执行失效,完全依赖服务端数据(适合非关键数据)
1052
- - 形状适配:通过 `consistency.familySync.listSelector` 适配 `{items,total}` 结构;无法识别时自动降级为仅失效
1053
-
1054
- ```tsx
1055
- import { useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
1056
- import { createPaginatedKey } from '@qiaopeng/tanstack-query-plus/core'
1057
-
1058
- function useUpdateProduct({ page, pageSize }) {
1059
- return useMutation({
1060
- mutationFn: (updated) => api.updateProduct(updated.id, updated),
1061
-
1062
- // 当前页的乐观更新:先更新 UI,再发请求,失败自动回滚
1063
- optimistic: {
1064
- queryKey: createPaginatedKey(['products', 'list'], page, pageSize),
1065
- updater: (old, updated) => old?.map((p) => (p.id === updated.id ? { ...p, ...updated } : p)),
1066
- },
1067
-
1068
- // 家族一致性:编辑成功后,保障跨分页/筛选/排序的变体不回退
1069
- consistency: {
1070
- mode: 'sync+invalidate', // 默认值,先同步缓存再延迟失效
1071
- invalidationDelay: 1000, // 延迟失效,等待后端一致性
1072
- familySync: {
1073
- idField: 'id',
1074
- // 适配分页对象:提取 items;不确定时返回 null 将仅失效
1075
- listSelector: (data) => {
1076
- if (data && typeof data === 'object' && 'items' in (data as any)) {
1077
- return { items: (data as any).items, total: (data as any).total }
1078
- }
1079
- if (Array.isArray(data)) return { items: data }
1080
- return null
1081
- },
1082
- maxKeys: 50,
1083
- }
1084
- },
1085
- })
1086
- }
1087
- ```
1088
-
1089
- 适用操作与行为说明:
1090
- - 编辑/删除:在 `sync+invalidate` 模式下,会对已缓存的家族变体按 `id` 合并或移除;随后统一失效,最终以服务端为准
1091
- - **自动竞态保护**:当检测到 `consistency` 模式开启时,会自动取消该列表家族下所有正在进行的旧请求,防止旧数据覆盖新缓存
1092
- - 新增/状态变更:默认不做跨页注入,仅当前页处理并家族失效;需要跨页放置时请在服务端裁决归属
1093
-
1094
1045
  现在你已经掌握了数据变更和乐观更新。接下来,让我们学习如何处理无限滚动和分页场景。
1095
1046
 
1096
1047
  ---
@@ -28,10 +28,6 @@ export interface DataGuardMutationOptions<TData, TError, TVariables, TContext> e
28
28
  * items: old?.items?.map(p => p.id === updated.id ? updated : p)
29
29
  * })
30
30
  * },
31
- * consistency: {
32
- * mode: 'sync+invalidate',
33
- * invalidationDelay: 3000
34
- * },
35
31
  * onConflict: (error) => {
36
32
  * toast.error('数据冲突,请刷新')
37
33
  * }
@@ -1 +1 @@
1
- {"version":3,"file":"useDataGuardMutation.d.ts","sourceRoot":"","sources":["../../src/hooks/useDataGuardMutation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAA8B,MAAM,uBAAuB,CAAC;AAOzF,MAAM,WAAW,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAE,SAAQ,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC;IACzI,aAAa;IACb,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,SAAS,eAAe,GAAG,eAAe,EAC/C,MAAM,GAAG,KAAK,EACd,UAAU,SAAS,eAAe,GAAG,eAAe,EACpD,QAAQ,GAAG,OAAO,EAElB,UAAU,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,KAAK,CAAC,EAChD,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,GACtE,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,CAmHxD"}
1
+ {"version":3,"file":"useDataGuardMutation.d.ts","sourceRoot":"","sources":["../../src/hooks/useDataGuardMutation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAA8B,MAAM,uBAAuB,CAAC;AAuBzF,MAAM,WAAW,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAE,SAAQ,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC;IACzI,aAAa;IACb,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,SAAS,eAAe,GAAG,eAAe,EAC/C,MAAM,GAAG,KAAK,EACd,UAAU,SAAS,eAAe,GAAG,eAAe,EACpD,QAAQ,GAAG,OAAO,EAElB,UAAU,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,KAAK,CAAC,EAChD,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,GACtE,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,CAkHxD"}
@@ -2,7 +2,21 @@ import { useQueryClient } from "@tanstack/react-query";
2
2
  import { useMutation } from "./useMutation.js";
3
3
  import { ConflictError } from "../types/dataGuard.js";
4
4
  import { hashObject, markRecentlyUpdated, clearRecentlyUpdated, updateFamilyMetadata } from "../utils/dataGuard.js";
5
- import { startsWithKeyPrefix } from "../utils/consistency.js";
5
+ /**
6
+ * Check if a query key starts with a given prefix
7
+ */
8
+ function startsWithKeyPrefix(key, prefix) {
9
+ const k = Array.isArray(key) ? key : [key];
10
+ const p = Array.isArray(prefix) ? prefix : [prefix];
11
+ if (p.length > k.length)
12
+ return false;
13
+ for (let i = 0; i < p.length; i++) {
14
+ if (JSON.stringify(k[i]) !== JSON.stringify(p[i])) {
15
+ return false;
16
+ }
17
+ }
18
+ return true;
19
+ }
6
20
  /**
7
21
  * 带数据防护的 Mutation Hook
8
22
  *
@@ -26,10 +40,6 @@ import { startsWithKeyPrefix } from "../utils/consistency.js";
26
40
  * items: old?.items?.map(p => p.id === updated.id ? updated : p)
27
41
  * })
28
42
  * },
29
- * consistency: {
30
- * mode: 'sync+invalidate',
31
- * invalidationDelay: 3000
32
- * },
33
43
  * onConflict: (error) => {
34
44
  * toast.error('数据冲突,请刷新')
35
45
  * }
@@ -91,9 +101,8 @@ export function useDataGuardMutation(mutationFn, queryKey, options) {
91
101
  // 更新所有家族缓存的元数据
92
102
  const familyKey = Array.isArray(queryKey) ? queryKey.slice(0, -1) : [queryKey];
93
103
  updateFamilyMetadata(queryClient, familyKey, data);
94
- // 计算清理延迟时间(基于 invalidationDelay + 缓冲时间)
95
- const invalidationDelay = options?.consistency?.invalidationDelay || 3000;
96
- const cleanupDelay = invalidationDelay + 2000; // 在失效后2秒清理
104
+ // 延迟清理最近更新标记(5秒后)
105
+ const cleanupDelay = 5000;
97
106
  // 延迟清理最近更新标记
98
107
  setTimeout(() => {
99
108
  try {
@@ -2,7 +2,6 @@ import type { MutationFunction, MutationKey, QueryClient, QueryKey, UseMutationO
2
2
  import type { MutationOptions } from "../types";
3
3
  import type { EntityWithId } from "../types/selectors";
4
4
  export type { MutationKey };
5
- import { FamilySyncConfig } from "../utils/consistency.js";
6
5
  export interface MutationDefaultsConfig {
7
6
  [key: string]: TanStackUseMutationOptions<any, any, any, any>;
8
7
  }
@@ -14,17 +13,11 @@ export declare function setupMutationDefaults(queryClient: QueryClient, config:
14
13
  export declare function useListMutation<T extends EntityWithId>(mutationFn: MutationFunction<T, {
15
14
  operation: string;
16
15
  data: Partial<T>;
17
- }>, queryKey: QueryKey, options?: (TanStackUseMutationOptions<T, Error, {
16
+ }>, queryKey: QueryKey, options?: TanStackUseMutationOptions<T, Error, {
18
17
  operation: string;
19
18
  data: Partial<T>;
20
19
  }> & {
21
20
  mutationKey?: readonly unknown[];
22
- }) & {
23
- consistency?: {
24
- familySync?: FamilySyncConfig;
25
- mode?: "sync+invalidate" | "invalidate-only" | "sync-only" | "auto";
26
- invalidationDelay?: number;
27
- };
28
21
  }): UseMutationResult<T, Error, {
29
22
  operation: string;
30
23
  data: Partial<T>;
@@ -1 +1 @@
1
- {"version":3,"file":"useMutation.d.ts","sourceRoot":"","sources":["../../src/hooks/useMutation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,kBAAkB,IAAI,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACvK,OAAO,KAAK,EAAmB,eAAe,EAAE,MAAM,UAAU,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,YAAY,EAAE,WAAW,EAAE,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAiF,MAAM,yBAAyB,CAAC;AAE1I,MAAM,WAAW,sBAAsB;IAAG,CAAC,GAAG,EAAE,MAAM,GAAG,0BAA0B,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;CAAE;AA4BzG,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,QAE5E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,QAExE;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,QAEvE;AAUD,wBAAgB,WAAW,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,UAAU,GAAG,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,OAAO,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,GAAG,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,CAgJzN;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAEpG;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,YAAY,EACpD,UAAU,EAAE,gBAAgB,CAAC,CAAC,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC,EACxE,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,EAAE,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC,GAAG;IAAE,WAAW,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CAAE,CAAC,GAAG;IACjI,WAAW,CAAC,EAAE;QACZ,UAAU,CAAC,EAAE,gBAAgB,CAAC;QAC9B,IAAI,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,WAAW,GAAG,MAAM,CAAC;QACpE,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAA;CACF;eAN4D,MAAM;UAAQ,OAAO,CAAC,CAAC,CAAC;YAoCtF"}
1
+ {"version":3,"file":"useMutation.d.ts","sourceRoot":"","sources":["../../src/hooks/useMutation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,kBAAkB,IAAI,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACvK,OAAO,KAAK,EAAmB,eAAe,EAAE,MAAM,UAAU,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,MAAM,WAAW,sBAAsB;IAAG,CAAC,GAAG,EAAE,MAAM,GAAG,0BAA0B,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;CAAE;AAkBzG,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,QAE5E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,QAExE;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,QAEvE;AAED,wBAAgB,WAAW,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,UAAU,GAAG,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,OAAO,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,GAAG,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,CAwIzN;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAEpG;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,YAAY,EACpD,UAAU,EAAE,gBAAgB,CAAC,CAAC,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC,EACxE,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,0BAA0B,CAAC,CAAC,EAAE,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC,GAAG;IAAE,WAAW,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CAAE;eAAlE,MAAM;UAAQ,OAAO,CAAC,CAAC,CAAC;YAarF"}
@@ -1,6 +1,5 @@
1
1
  import { useQueryClient, useMutation as useTanStackMutation } from "@tanstack/react-query";
2
2
  import { DEFAULT_MUTATION_CONFIG } from "../core/config.js";
3
- import { syncEntityAcrossFamily, syncEntityAcrossFamilyOptimistic, DEFAULT_FAMILY_SYNC } from "../utils/consistency.js";
4
3
  function deriveFamilyKey(queryKey) {
5
4
  const arr = Array.isArray(queryKey) ? [...queryKey] : [queryKey];
6
5
  while (arr.length > 1) {
@@ -21,18 +20,6 @@ function isListFamilyKey(queryKey) {
21
20
  const parts = Array.isArray(queryKey) ? queryKey : [queryKey];
22
21
  return parts.includes("list") || parts.includes("paginated");
23
22
  }
24
- function getConsistencyStrategy(mode, op) {
25
- const m = mode ?? "sync+invalidate";
26
- if (m === "sync-only")
27
- return { sync: true, invalidate: false };
28
- if (m === "invalidate-only")
29
- return { sync: false, invalidate: true };
30
- // Auto mode: Always sync for immediate UI feedback, and always invalidate for eventual consistency
31
- // Note: The invalidation should ideally be delayed to avoid old-data flicker (handled in delay logic)
32
- if (m === "auto")
33
- return { sync: true, invalidate: true };
34
- return { sync: true, invalidate: true };
35
- }
36
23
  export function invalidateQueriesBatch(queryClient, tasks) {
37
24
  tasks.forEach(task => queryClient.invalidateQueries(task));
38
25
  }
@@ -42,17 +29,9 @@ export function cancelQueriesBatch(queryClient, tasks) {
42
29
  export function setQueryDataBatch(queryClient, tasks) {
43
30
  tasks.forEach(task => queryClient.setQueryData(task.queryKey, task.data));
44
31
  }
45
- function executeInvalidations(queryClient, tasks) {
46
- if (tasks.length === 1) {
47
- queryClient.invalidateQueries(tasks[0]);
48
- }
49
- else if (tasks.length > 1) {
50
- invalidateQueriesBatch(queryClient, tasks);
51
- }
52
- }
53
32
  export function useMutation(options) {
54
33
  const queryClient = useQueryClient();
55
- const { optimistic, onMutate, onError, onSuccess, onSettled, consistency, ...restOptions } = options;
34
+ const { optimistic, onMutate, onError, onSuccess, onSettled, ...restOptions } = options;
56
35
  const mutationConfig = {
57
36
  ...restOptions,
58
37
  retry: restOptions.retry ?? DEFAULT_MUTATION_CONFIG?.retry,
@@ -81,14 +60,13 @@ export function useMutation(options) {
81
60
  return { userContext };
82
61
  }
83
62
  try {
84
- await queryClient.cancelQueries({ queryKey: optimistic.queryKey, exact: true });
85
- // Cancel family queries to prevent race conditions:
86
- // Ensure no outdated refetches overwrite our optimistic/synced updates
87
- if (consistency?.mode !== "invalidate-only") {
88
- const familyKey = optimistic.familyKey ?? deriveFamilyKey(optimistic.queryKey);
89
- await queryClient.cancelQueries({ queryKey: familyKey });
90
- }
91
- const previousData = queryClient.getQueryData(optimistic.queryKey);
63
+ // Derive family key (e.g., ['products', 'list'] from ['products', 'list', { page: 1, pageSize: 10 }])
64
+ const familyKey = optimistic.familyKey ?? deriveFamilyKey(optimistic.queryKey);
65
+ // Cancel all queries in the family to prevent race conditions
66
+ await queryClient.cancelQueries({ queryKey: familyKey });
67
+ // Save snapshots of all queries in the family
68
+ const previousData = queryClient.getQueriesData({ queryKey: familyKey });
69
+ // Apply field mapping if specified
92
70
  let mappedVariables = variables;
93
71
  if (optimistic.fieldMapping && typeof variables === "object" && variables !== null) {
94
72
  mappedVariables = { ...variables };
@@ -100,36 +78,34 @@ export function useMutation(options) {
100
78
  }
101
79
  });
102
80
  }
103
- queryClient.setQueryData(optimistic.queryKey, (oldData) => optimistic.updater(oldData, mappedVariables));
104
- let familyRollbackData = [];
105
- const op = (typeof variables?.operation === "string" ? variables.operation : consistency?.defaultOperation) ?? "update";
106
- const { sync } = getConsistencyStrategy(consistency?.mode, op);
107
- if (sync && optimistic?.queryKey) {
108
- const familyKey = optimistic.familyKey ?? deriveFamilyKey(optimistic.queryKey);
109
- const payload = typeof variables === "object" && variables !== null ? (variables.data ?? variables) : variables;
110
- const cfg = consistency?.familySync ?? DEFAULT_FAMILY_SYNC;
111
- familyRollbackData = syncEntityAcrossFamilyOptimistic(queryClient, familyKey, cfg, op, payload);
112
- }
81
+ // Official approach: Update all cached query variants using setQueriesData
82
+ queryClient.setQueriesData({ queryKey: familyKey }, (oldData) => {
83
+ if (!oldData)
84
+ return oldData;
85
+ return optimistic.updater(oldData, mappedVariables);
86
+ });
113
87
  const mutateCallback = onMutate;
114
88
  const userContext = onMutate ? await mutateCallback(variables) : undefined;
115
- return { previousData, userContext, familyRollbackData };
89
+ return { previousData, userContext };
116
90
  }
117
91
  catch (error) {
118
92
  return { userContext: undefined };
119
93
  }
120
94
  };
121
95
  mutationConfig.onError = (error, variables, context) => {
122
- if (context?.previousData !== undefined) {
123
- queryClient.setQueryData(optimistic.queryKey, context.previousData);
124
- }
125
- if (context?.familyRollbackData) {
126
- context.familyRollbackData.forEach(item => {
127
- queryClient.setQueryData(item.queryKey, item.previousData);
96
+ // Rollback all query variants
97
+ if (context?.previousData) {
98
+ context.previousData.forEach(([queryKey, data]) => {
99
+ queryClient.setQueryData(queryKey, data);
128
100
  });
129
101
  }
130
- if (optimistic.rollback && context?.previousData !== undefined) {
102
+ if (optimistic.rollback && context?.previousData) {
131
103
  try {
132
- optimistic.rollback(context.previousData, error);
104
+ // Call rollback with the first snapshot (usually the current query)
105
+ const firstSnapshot = context.previousData[0]?.[1];
106
+ if (firstSnapshot) {
107
+ optimistic.rollback(firstSnapshot, error);
108
+ }
133
109
  }
134
110
  catch { }
135
111
  }
@@ -139,33 +115,26 @@ export function useMutation(options) {
139
115
  }
140
116
  };
141
117
  mutationConfig.onSuccess = (data, variables, context) => {
142
- const op = (typeof variables?.operation === "string" ? variables.operation : consistency?.defaultOperation) ?? "update";
143
- const { sync, invalidate } = getConsistencyStrategy(consistency?.mode, op);
144
- if (sync && optimistic?.queryKey) {
145
- const familyKey = optimistic.familyKey ?? deriveFamilyKey(optimistic.queryKey);
146
- const payload = typeof variables === "object" && variables !== null ? (variables.data ?? variables) : variables;
147
- const cfg = consistency?.familySync ?? DEFAULT_FAMILY_SYNC;
148
- syncEntityAcrossFamily(queryClient, familyKey, cfg, op, payload);
149
- }
118
+ // Determine invalidation scope
150
119
  const scope = optimistic.invalidateScope ?? (isListFamilyKey(optimistic.queryKey) ? "family" : "exact");
151
120
  const invalidations = [];
152
- if (invalidate) {
153
- if (scope !== "none") {
154
- if (scope === "family") {
155
- const familyKey = optimistic.familyKey ?? deriveFamilyKey(optimistic.queryKey);
156
- invalidations.push({ queryKey: familyKey });
157
- }
158
- else {
159
- invalidations.push({ queryKey: optimistic.queryKey });
160
- }
161
- }
162
- if (Array.isArray(optimistic.relatedKeys) && optimistic.relatedKeys.length > 0) {
163
- optimistic.relatedKeys.forEach((k) => invalidations.push({ queryKey: k }));
121
+ if (scope !== "none") {
122
+ if (scope === "family") {
123
+ const familyKey = optimistic.familyKey ?? deriveFamilyKey(optimistic.queryKey);
124
+ invalidations.push({ queryKey: familyKey });
164
125
  }
165
- if (Array.isArray(optimistic.invalidates) && optimistic.invalidates.length > 0) {
166
- optimistic.invalidates.forEach((k) => invalidations.push({ queryKey: k }));
126
+ else {
127
+ invalidations.push({ queryKey: optimistic.queryKey });
167
128
  }
168
129
  }
130
+ // Add related keys
131
+ if (Array.isArray(optimistic.relatedKeys) && optimistic.relatedKeys.length > 0) {
132
+ optimistic.relatedKeys.forEach((k) => invalidations.push({ queryKey: k }));
133
+ }
134
+ if (Array.isArray(optimistic.invalidates) && optimistic.invalidates.length > 0) {
135
+ optimistic.invalidates.forEach((k) => invalidations.push({ queryKey: k }));
136
+ }
137
+ // Execute invalidations (deduplicated)
169
138
  if (invalidations.length > 0) {
170
139
  const seen = new Set();
171
140
  const tasks = invalidations
@@ -177,13 +146,7 @@ export function useMutation(options) {
177
146
  seen.add(key);
178
147
  return true;
179
148
  });
180
- const delay = consistency?.invalidationDelay ?? (consistency?.mode === "auto" ? 1000 : 0);
181
- if (delay > 0) {
182
- setTimeout(() => executeInvalidations(queryClient, tasks), delay);
183
- }
184
- else {
185
- executeInvalidations(queryClient, tasks);
186
- }
149
+ tasks.forEach(task => queryClient.invalidateQueries(task));
187
150
  }
188
151
  if (onSuccess) {
189
152
  const successCallback = onSuccess;
@@ -206,28 +169,10 @@ export function useListMutation(mutationFn, queryKey, options) {
206
169
  const queryClient = useQueryClient();
207
170
  return useTanStackMutation({
208
171
  mutationFn,
209
- onSuccess: (_data, variables) => {
210
- const { sync } = getConsistencyStrategy(options?.consistency?.mode, variables.operation);
211
- if (sync) {
212
- const familyKey = deriveFamilyKey(queryKey);
213
- const cfg = options?.consistency?.familySync ?? DEFAULT_FAMILY_SYNC;
214
- syncEntityAcrossFamily(queryClient, familyKey, cfg, variables.operation, variables.data);
215
- }
216
- },
217
- onSettled: (data, error, variables) => {
218
- const { invalidate } = getConsistencyStrategy(options?.consistency?.mode, variables.operation);
219
- if (invalidate) {
220
- const familyKey = deriveFamilyKey(queryKey);
221
- const delay = options?.consistency?.invalidationDelay ?? (options?.consistency?.mode === "auto" ? 1000 : 0);
222
- if (delay > 0) {
223
- setTimeout(() => {
224
- queryClient.invalidateQueries({ queryKey: familyKey, exact: false });
225
- }, delay);
226
- }
227
- else {
228
- queryClient.invalidateQueries({ queryKey: familyKey, exact: false });
229
- }
230
- }
172
+ onSettled: () => {
173
+ // Invalidate all queries in the family
174
+ const familyKey = deriveFamilyKey(queryKey);
175
+ queryClient.invalidateQueries({ queryKey: familyKey, exact: false });
231
176
  },
232
177
  ...options,
233
178
  mutationKey: options?.mutationKey
@@ -8,56 +8,46 @@ export * from "./persistence.js";
8
8
  export * from "./selectors.js";
9
9
  export * from "./suspense.js";
10
10
  export interface MutationContext<TData = unknown, TContext = unknown> {
11
- previousData?: TData;
11
+ /** Snapshots of all query variants (from getQueriesData) */
12
+ previousData?: Array<[QueryKey, any]>;
13
+ /** User-provided context from onMutate */
12
14
  userContext?: TContext;
15
+ /** For conditional mutations */
13
16
  conditionMet?: boolean;
14
- familyRollbackData?: Array<{
15
- queryKey: QueryKey;
16
- previousData: unknown;
17
- }>;
18
17
  }
18
+ /**
19
+ * Enhanced mutation options using official TanStack Query approach
20
+ *
21
+ * Uses setQueriesData to update all cached query variants automatically.
22
+ * This prevents data rollback and flicker when switching between pageSize, filters, etc.
23
+ *
24
+ * Recommended: Use long staleTime (5+ minutes) in your queries for best results.
25
+ */
19
26
  export interface MutationOptions<TData, TError, TVariables, TContext = unknown> extends UseMutationOptions<TData, TError, TVariables, TContext> {
20
27
  optimistic?: {
28
+ /** Query key to update (will also update all variants in the same family) */
21
29
  queryKey: QueryKey;
30
+ /** Function to update the cached data - will be applied to all query variants */
22
31
  updater: <TQueryData = unknown>(oldData: TQueryData | undefined, variables: TVariables) => TQueryData | undefined;
32
+ /** Enable/disable optimistic updates */
23
33
  enabled?: boolean;
34
+ /** Map mutation variable fields to cache data fields */
24
35
  fieldMapping?: Record<string, string>;
36
+ /** Callback when rollback occurs on error */
25
37
  rollback?: <TQueryData = unknown>(previousData: TQueryData, error: Error) => void;
38
+ /**
39
+ * Invalidation scope after success:
40
+ * - "exact": Only invalidate the exact queryKey
41
+ * - "family": Invalidate all queries in the family (default for list queries)
42
+ * - "none": Don't invalidate (rely on optimistic update only)
43
+ */
26
44
  invalidateScope?: "none" | "exact" | "family";
45
+ /** Override the derived family key (e.g., ['products', 'list']) */
27
46
  familyKey?: QueryKey;
47
+ /** Additional query keys to invalidate */
28
48
  relatedKeys?: QueryKey[];
49
+ /** Alias for relatedKeys */
29
50
  invalidates?: QueryKey[];
30
51
  };
31
- consistency?: {
32
- familySync?: {
33
- idField?: string;
34
- listSelector?: (data: unknown) => {
35
- items: unknown[];
36
- total?: number;
37
- } | null;
38
- writeBack?: (old: unknown, items: unknown[], total?: number) => unknown;
39
- maxKeys?: number;
40
- enableForOperations?: Array<"update" | "delete">;
41
- };
42
- /**
43
- * Consistency strategy:
44
- * - "sync+invalidate": Update cache locally AND invalidate queries (default).
45
- * - "sync-only": Update cache locally only (use when server is eventually consistent and you trust the client).
46
- * - "invalidate-only": Do not update cache locally, just invalidate.
47
- * - "auto": "sync-only" for updates, "sync+invalidate" for deletes.
48
- */
49
- mode?: "sync+invalidate" | "invalidate-only" | "sync-only" | "auto";
50
- /**
51
- * Default operation type if not provided in variables.
52
- * Useful for useMutation where variables might not contain operation info.
53
- */
54
- defaultOperation?: "create" | "update" | "delete";
55
- /**
56
- * Delay in milliseconds before invalidating queries.
57
- * Useful for eventually consistent backends (e.g. ElasticSearch).
58
- * Default: 0
59
- */
60
- invalidationDelay?: number;
61
- };
62
52
  }
63
53
  //# 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,gBAAgB,CAAC;AAC/B,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,CAAC;IAAC,kBAAkB,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAAE;AAC1N,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;QACX,QAAQ,EAAE,QAAQ,CAAC;QACnB,OAAO,EAAE,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,EAAE,UAAU,GAAG,SAAS,EAAE,SAAS,EAAE,UAAU,KAAK,UAAU,GAAG,SAAS,CAAC;QAClH,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,QAAQ,CAAC,EAAE,CAAC,UAAU,GAAG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;QAClF,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;QAC9C,SAAS,CAAC,EAAE,QAAQ,CAAC;QACrB,WAAW,CAAC,EAAE,QAAQ,EAAE,CAAC;QACzB,WAAW,CAAC,EAAE,QAAQ,EAAE,CAAC;KAC1B,CAAC;IACF,WAAW,CAAC,EAAE;QACZ,UAAU,CAAC,EAAE;YACX,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK;gBAAE,KAAK,EAAE,OAAO,EAAE,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,GAAG,IAAI,CAAC;YAC9E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;YACxE,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,mBAAmB,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC;SAClD,CAAC;QACF;;;;;;WAMG;QACH,IAAI,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,WAAW,GAAG,MAAM,CAAC;QACpE;;;WAGG;QACH,gBAAgB,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAClD;;;;WAIG;QACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC;CACH"}
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,gBAAgB,CAAC;AAC/B,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;IAClE,4DAA4D;IAC5D,YAAY,CAAC,EAAE,KAAK,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IACtC,0CAA0C;IAC1C,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,gCAAgC;IAChC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AACD;;;;;;;GAOG;AACH,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;QACX,6EAA6E;QAC7E,QAAQ,EAAE,QAAQ,CAAC;QACnB,iFAAiF;QACjF,OAAO,EAAE,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,EAAE,UAAU,GAAG,SAAS,EAAE,SAAS,EAAE,UAAU,KAAK,UAAU,GAAG,SAAS,CAAC;QAClH,wCAAwC;QACxC,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,wDAAwD;QACxD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,6CAA6C;QAC7C,QAAQ,CAAC,EAAE,CAAC,UAAU,GAAG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;QAClF;;;;;WAKG;QACH,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;QAC9C,mEAAmE;QACnE,SAAS,CAAC,EAAE,QAAQ,CAAC;QACrB,0CAA0C;QAC1C,WAAW,CAAC,EAAE,QAAQ,EAAE,CAAC;QACzB,4BAA4B;QAC5B,WAAW,CAAC,EAAE,QAAQ,EAAE,CAAC;KAC1B,CAAC;CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"dataGuard.d.ts","sourceRoot":"","sources":["../../src/utils/dataGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,0BAA0B,EAC3B,MAAM,uBAAuB,CAAC;AA+B/B;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,OAAO,EAAE,0BAA0B,CAAC,CAAC,CAAC,EACtC,MAAM,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,SAAS,EACjD,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,gBAAqB,GAC7B;IACD,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,YAAY,EAAE,GAAG,CAAC;CACnB,CA+IA;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,eAAe,EACrD,IAAI,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAClC,0BAA0B,CAAC,CAAC,CAAC,CAK/B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,eAAe,EAC3D,IAAI,EAAE,0BAA0B,CAAC,CAAC,CAAC,EACnC,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,0BAA0B,CAAC,CAAC,CAAC,CAQ/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,eAAe,EAC5D,IAAI,EAAE,0BAA0B,CAAC,CAAC,CAAC,EACnC,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,0BAA0B,CAAC,CAAC,CAAC,CAU/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,eAAe,EAC5D,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,QAAQ,EACnB,QAAQ,EAAE,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,GAC/C,IAAI,CAiCN"}
1
+ {"version":3,"file":"dataGuard.d.ts","sourceRoot":"","sources":["../../src/utils/dataGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,0BAA0B,EAC3B,MAAM,uBAAuB,CAAC;AA+C/B;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,OAAO,EAAE,0BAA0B,CAAC,CAAC,CAAC,EACtC,MAAM,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,SAAS,EACjD,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,gBAAqB,GAC7B;IACD,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,YAAY,EAAE,GAAG,CAAC;CACnB,CA+IA;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,eAAe,EACrD,IAAI,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAClC,0BAA0B,CAAC,CAAC,CAAC,CAK/B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,eAAe,EAC3D,IAAI,EAAE,0BAA0B,CAAC,CAAC,CAAC,EACnC,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,0BAA0B,CAAC,CAAC,CAAC,CAQ/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,eAAe,EAC5D,IAAI,EAAE,0BAA0B,CAAC,CAAC,CAAC,EACnC,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,0BAA0B,CAAC,CAAC,CAAC,CAU/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,eAAe,EAC5D,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,QAAQ,EACnB,QAAQ,EAAE,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,GAC/C,IAAI,CAiCN"}
@@ -1,4 +1,18 @@
1
- import { startsWithKeyPrefix } from "./consistency.js";
1
+ /**
2
+ * Check if a query key starts with a given prefix
3
+ */
4
+ function startsWithKeyPrefix(key, prefix) {
5
+ const k = Array.isArray(key) ? key : [key];
6
+ const p = Array.isArray(prefix) ? prefix : [prefix];
7
+ if (p.length > k.length)
8
+ return false;
9
+ for (let i = 0; i < p.length; i++) {
10
+ if (JSON.stringify(k[i]) !== JSON.stringify(p[i])) {
11
+ return false;
12
+ }
13
+ }
14
+ return true;
15
+ }
2
16
  /**
3
17
  * 简单的字符串哈希函数(DJB2算法变体)
4
18
  */
@@ -6,6 +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 { startsWithKeyPrefix, syncEntityAcrossFamily, DEFAULT_FAMILY_SYNC, type FamilySyncConfig } from "./consistency.js";
10
9
  export { applyDataGuard, addHashToData, hashObject, markRecentlyUpdated, clearRecentlyUpdated, updateFamilyMetadata } from "./dataGuard.js";
11
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,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,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,mBAAmB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC3H,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,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,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,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,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -6,5 +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 { startsWithKeyPrefix, syncEntityAcrossFamily, DEFAULT_FAMILY_SYNC } from "./consistency.js";
10
9
  export { applyDataGuard, addHashToData, hashObject, markRecentlyUpdated, clearRecentlyUpdated, updateFamilyMetadata } from "./dataGuard.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qiaopeng/tanstack-query-plus",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Enhanced TanStack Query toolkit: defaults, hooks, persistence, offline, data guard, utils",
5
5
  "author": "qiaopeng",
6
6
  "license": "MIT",
@@ -1,25 +0,0 @@
1
- import type { QueryClient, QueryKey } from "@tanstack/react-query";
2
- import type { EntityWithId } from "../types/selectors";
3
- export declare function startsWithKeyPrefix(key: QueryKey, prefix: QueryKey): boolean;
4
- export type FamilySyncConfig = {
5
- idField?: string;
6
- listSelector?: (data: unknown) => {
7
- items: EntityWithId[];
8
- total?: number;
9
- } | null;
10
- writeBack?: (old: unknown, items: EntityWithId[], total?: number) => unknown;
11
- maxKeys?: number;
12
- enableForOperations?: Array<"update" | "delete">;
13
- };
14
- export declare function defaultListSelector(data: unknown): {
15
- items: EntityWithId[];
16
- total?: number;
17
- } | null;
18
- export declare function defaultWriteBack(old: unknown, items: EntityWithId[], total?: number): unknown;
19
- export declare const DEFAULT_FAMILY_SYNC: FamilySyncConfig;
20
- export declare function syncEntityAcrossFamily(queryClient: QueryClient, familyPrefix: QueryKey, cfg: FamilySyncConfig, operation: string, payload: Partial<EntityWithId>): void;
21
- export declare function syncEntityAcrossFamilyOptimistic(queryClient: QueryClient, familyPrefix: QueryKey, cfg: FamilySyncConfig, operation: string, payload: Partial<EntityWithId>): Array<{
22
- queryKey: QueryKey;
23
- previousData: unknown;
24
- }>;
25
- //# sourceMappingURL=consistency.d.ts.map
@@ -1 +0,0 @@
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;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAwCvD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAgB5E;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK;QAAE,KAAK,EAAE,YAAY,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC;CAClD,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,YAAY,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAiBnG;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAe7F;AAED,eAAO,MAAM,mBAAmB,EAAE,gBAMjC,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,QAAQ,EACtB,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,GAC7B,IAAI,CAuCN;AAED,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,QAAQ,EACtB,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,GAC7B,KAAK,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC,CAoDtD"}
@@ -1,183 +0,0 @@
1
- import { listUpdater, idsAreEqual } from "./optimisticUtils.js";
2
- function ensureArray(key) {
3
- return Array.isArray(key) ? key : [key];
4
- }
5
- /**
6
- * Robust deep equality check that handles object key order and common types
7
- */
8
- function deepEqual(a, b) {
9
- if (a === b)
10
- return true;
11
- if (a && b && typeof a === "object" && typeof b === "object") {
12
- if (Array.isArray(a)) {
13
- if (!Array.isArray(b) || a.length !== b.length)
14
- return false;
15
- for (let i = 0; i < a.length; i++) {
16
- if (!deepEqual(a[i], b[i]))
17
- return false;
18
- }
19
- return true;
20
- }
21
- // Handle plain objects
22
- const keysA = Object.keys(a);
23
- const keysB = Object.keys(b);
24
- if (keysA.length !== keysB.length)
25
- return false;
26
- // Check if all keys in A exist in B and values are deep equal
27
- // Key order doesn't matter here because we look up by key
28
- for (const key of keysA) {
29
- if (!Object.prototype.hasOwnProperty.call(b, key))
30
- return false;
31
- if (!deepEqual(a[key], b[key]))
32
- return false;
33
- }
34
- return true;
35
- }
36
- return false;
37
- }
38
- export function startsWithKeyPrefix(key, prefix) {
39
- const k = ensureArray(key);
40
- const p = ensureArray(prefix);
41
- if (p.length > k.length)
42
- return false;
43
- for (let i = 0; i < p.length; i++) {
44
- const a = k[i];
45
- const b = p[i];
46
- // Check for equality using robust deepEqual
47
- if (!deepEqual(a, b)) {
48
- return false;
49
- }
50
- }
51
- return true;
52
- }
53
- export function defaultListSelector(data) {
54
- if (!data)
55
- return null;
56
- if (Array.isArray(data))
57
- return { items: data };
58
- if (typeof data === "object") {
59
- const obj = data;
60
- if (Array.isArray(obj.items))
61
- return {
62
- items: obj.items,
63
- total: typeof obj.total === "number" ? obj.total : undefined
64
- };
65
- if (Array.isArray(obj.Rows))
66
- return {
67
- items: obj.Rows,
68
- total: typeof obj.Total === "number" ? obj.Total : undefined
69
- };
70
- }
71
- return null;
72
- }
73
- export function defaultWriteBack(old, items, total) {
74
- if (old && typeof old === "object") {
75
- const obj = { ...old };
76
- if (Array.isArray(obj.Rows)) {
77
- obj.Rows = items;
78
- if (typeof total !== "undefined")
79
- obj.Total = total;
80
- return obj;
81
- }
82
- if (Array.isArray(obj.items)) {
83
- obj.items = items;
84
- if (typeof total !== "undefined")
85
- obj.total = total;
86
- return obj;
87
- }
88
- }
89
- return { items, total };
90
- }
91
- export const DEFAULT_FAMILY_SYNC = {
92
- idField: "id",
93
- listSelector: defaultListSelector,
94
- writeBack: defaultWriteBack,
95
- maxKeys: 50,
96
- enableForOperations: ["update", "delete"]
97
- };
98
- export function syncEntityAcrossFamily(queryClient, familyPrefix, cfg, operation, payload) {
99
- const enabledOps = cfg.enableForOperations ?? ["update", "delete"];
100
- const op = String(operation).toLowerCase();
101
- if (!enabledOps.includes(op))
102
- return;
103
- const idField = cfg.idField ?? "id";
104
- const cache = queryClient.getQueryCache();
105
- const queries = cache.findAll({ predicate: (q) => startsWithKeyPrefix(q.queryKey, familyPrefix) });
106
- const limited = typeof cfg.maxKeys === "number" && cfg.maxKeys > 0 ? queries.slice(0, cfg.maxKeys) : queries;
107
- limited.forEach((q) => {
108
- const key = q.queryKey;
109
- const old = queryClient.getQueryData(key);
110
- const picked = (cfg.listSelector ?? defaultListSelector)(old);
111
- if (!picked || !Array.isArray(picked.items))
112
- return;
113
- const idValue = (typeof payload === "string" || typeof payload === "number") ? payload : payload?.[idField];
114
- let items = picked.items;
115
- if (op === "update" && idValue !== undefined) {
116
- const existingItem = items.find(item => idsAreEqual(item[idField], idValue));
117
- if (existingItem) {
118
- const strictId = existingItem[idField];
119
- if (typeof payload === "object") {
120
- items = listUpdater.update(items, { ...payload, [idField]: strictId });
121
- }
122
- }
123
- }
124
- else if (op === "delete" && idValue !== undefined) {
125
- const existingItem = items.find(item => idsAreEqual(item[idField], idValue));
126
- if (existingItem) {
127
- const strictId = existingItem[idField];
128
- items = listUpdater.remove(items, strictId);
129
- }
130
- }
131
- const total = typeof picked.total === "number" && op === "delete" ? Math.max(0, picked.total - 1) : picked.total;
132
- const next = (cfg.writeBack ?? defaultWriteBack)(old, items, total);
133
- queryClient.setQueryData(key, next);
134
- });
135
- }
136
- export function syncEntityAcrossFamilyOptimistic(queryClient, familyPrefix, cfg, operation, payload) {
137
- const rollbackData = [];
138
- const enabledOps = cfg.enableForOperations ?? ["update", "delete"];
139
- const op = String(operation).toLowerCase();
140
- if (!enabledOps.includes(op))
141
- return rollbackData;
142
- const idField = cfg.idField ?? "id";
143
- const cache = queryClient.getQueryCache();
144
- const queries = cache.findAll({ predicate: (q) => startsWithKeyPrefix(q.queryKey, familyPrefix) });
145
- const limited = typeof cfg.maxKeys === "number" && cfg.maxKeys > 0 ? queries.slice(0, cfg.maxKeys) : queries;
146
- limited.forEach((q) => {
147
- const key = q.queryKey;
148
- const old = queryClient.getQueryData(key);
149
- const picked = (cfg.listSelector ?? defaultListSelector)(old);
150
- if (!picked || !Array.isArray(picked.items))
151
- return;
152
- const idValue = (typeof payload === "string" || typeof payload === "number") ? payload : payload?.[idField];
153
- let items = picked.items;
154
- let shouldUpdate = false;
155
- if (op === "update" && idValue !== undefined) {
156
- // Robust ID check
157
- const existingItem = items.find(item => idsAreEqual(item[idField], idValue));
158
- if (existingItem) {
159
- const strictId = existingItem[idField];
160
- // Ensure payload uses the correct ID type from the store
161
- if (typeof payload === "object") {
162
- items = listUpdater.update(items, { ...payload, [idField]: strictId });
163
- shouldUpdate = true;
164
- }
165
- }
166
- }
167
- else if (op === "delete" && idValue !== undefined) {
168
- const existingItem = items.find(item => idsAreEqual(item[idField], idValue));
169
- if (existingItem) {
170
- const strictId = existingItem[idField];
171
- items = listUpdater.remove(items, strictId);
172
- shouldUpdate = true;
173
- }
174
- }
175
- if (shouldUpdate) {
176
- rollbackData.push({ queryKey: key, previousData: old });
177
- const total = typeof picked.total === "number" && op === "delete" ? Math.max(0, picked.total - 1) : picked.total;
178
- const next = (cfg.writeBack ?? defaultWriteBack)(old, items, total);
179
- queryClient.setQueryData(key, next);
180
- }
181
- });
182
- return rollbackData;
183
- }