@qiaopeng/tanstack-query-plus 0.3.1 → 0.4.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.
@@ -5,4 +5,6 @@ export { cancelQueriesBatch, invalidateQueriesBatch, type MutationDefaultsConfig
5
5
  export { type HoverPrefetchOptions, type InViewPrefetchOptions, type PrefetchOptions, useBatchPrefetch, useConditionalPrefetch, useHoverPrefetch, useIdlePrefetch, usePeriodicPrefetch, usePredictivePrefetch, usePriorityPrefetch, useRoutePrefetch, useSmartPrefetch } from "./usePrefetch.js";
6
6
  export { skipToken, useEnhancedQuery, type EnhancedQueryOptions, type EnhancedQueryResult } from "./useQuery.js";
7
7
  export { createSuspenseInfiniteQuery, createSuspenseQuery, useEnhancedSuspenseInfiniteQuery, useEnhancedSuspenseQuery } from "./useSuspenseQuery.js";
8
+ export { useDataGuardQueryConfig } from "./useDataGuardQuery.js";
9
+ export { useDataGuardMutation, type DataGuardMutationOptions } from "./useDataGuardMutation.js";
8
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,0BAA0B,EAC1B,wBAAwB,EACxB,kBAAkB,EAClB,0BAA0B,EAC1B,mBAAmB,EACnB,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,eAAe,EACf,KAAK,sBAAsB,EAC3B,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,KAAK,oBAAoB,EACzB,oBAAoB,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,6BAA6B,EAAE,0BAA0B,EAAE,6BAA6B,EAAE,iCAAiC,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC9L,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,WAAW,EACZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjH,OAAO,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,gCAAgC,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,0BAA0B,EAC1B,wBAAwB,EACxB,kBAAkB,EAClB,0BAA0B,EAC1B,mBAAmB,EACnB,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,eAAe,EACf,KAAK,sBAAsB,EAC3B,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,KAAK,oBAAoB,EACzB,oBAAoB,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,6BAA6B,EAAE,0BAA0B,EAAE,6BAA6B,EAAE,iCAAiC,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC9L,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,WAAW,EACZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjH,OAAO,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,gCAAgC,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACrJ,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -5,3 +5,5 @@ export { cancelQueriesBatch, invalidateQueriesBatch, setQueryDataBatch, setupMut
5
5
  export { useBatchPrefetch, useConditionalPrefetch, useHoverPrefetch, useIdlePrefetch, usePeriodicPrefetch, usePredictivePrefetch, usePriorityPrefetch, useRoutePrefetch, useSmartPrefetch } from "./usePrefetch.js";
6
6
  export { skipToken, useEnhancedQuery } from "./useQuery.js";
7
7
  export { createSuspenseInfiniteQuery, createSuspenseQuery, useEnhancedSuspenseInfiniteQuery, useEnhancedSuspenseQuery } from "./useSuspenseQuery.js";
8
+ export { useDataGuardQueryConfig } from "./useDataGuardQuery.js";
9
+ export { useDataGuardMutation } from "./useDataGuardMutation.js";
@@ -0,0 +1,43 @@
1
+ import type { QueryKey, UseMutationResult } from "@tanstack/react-query";
2
+ import type { MutationOptions } from "../types/index.js";
3
+ import type { VersionedEntity } from "../types/dataGuard.js";
4
+ export interface DataGuardMutationOptions<TData, TError, TVariables, TContext> extends MutationOptions<TData, TError, TVariables, TContext> {
5
+ /** 冲突错误回调 */
6
+ onConflict?: (error: any) => void;
7
+ }
8
+ /**
9
+ * 带数据防护的 Mutation Hook
10
+ *
11
+ * 自动处理:
12
+ * 1. 乐观更新时递增版本号和更新时间戳
13
+ * 2. 标记最近更新的项
14
+ * 3. 成功后更新所有家族缓存的元数据
15
+ * 4. 冲突检测(409 错误)
16
+ * 5. 延迟清理更新标记
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const mutation = useDataGuardMutation(
21
+ * (updated) => api.updateProduct(updated.id, updated),
22
+ * ['products', 'list', { page: 1, pageSize: 20 }],
23
+ * {
24
+ * optimistic: {
25
+ * queryKey: ['products', 'list', { page: 1, pageSize: 20 }],
26
+ * updater: (old, updated) => ({
27
+ * ...old,
28
+ * items: old?.items?.map(p => p.id === updated.id ? updated : p)
29
+ * })
30
+ * },
31
+ * consistency: {
32
+ * mode: 'sync+invalidate',
33
+ * invalidationDelay: 3000
34
+ * },
35
+ * onConflict: (error) => {
36
+ * toast.error('数据冲突,请刷新')
37
+ * }
38
+ * }
39
+ * )
40
+ * ```
41
+ */
42
+ export declare function useDataGuardMutation<TData extends VersionedEntity = VersionedEntity, TError = Error, TVariables extends VersionedEntity = VersionedEntity, TContext = unknown>(mutationFn: (data: TVariables) => Promise<TData>, queryKey: QueryKey, options?: DataGuardMutationOptions<TData, TError, TVariables, TContext>): UseMutationResult<TData, TError, TVariables, TContext>;
43
+ //# sourceMappingURL=useDataGuardMutation.d.ts.map
@@ -0,0 +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,CA2GxD"}
@@ -0,0 +1,132 @@
1
+ import { useQueryClient } from "@tanstack/react-query";
2
+ import { useMutation } from "./useMutation.js";
3
+ import { ConflictError } from "../types/dataGuard.js";
4
+ import { hashObject, markRecentlyUpdated, clearRecentlyUpdated, updateFamilyMetadata } from "../utils/dataGuard.js";
5
+ import { startsWithKeyPrefix } from "../utils/consistency.js";
6
+ /**
7
+ * 带数据防护的 Mutation Hook
8
+ *
9
+ * 自动处理:
10
+ * 1. 乐观更新时递增版本号和更新时间戳
11
+ * 2. 标记最近更新的项
12
+ * 3. 成功后更新所有家族缓存的元数据
13
+ * 4. 冲突检测(409 错误)
14
+ * 5. 延迟清理更新标记
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const mutation = useDataGuardMutation(
19
+ * (updated) => api.updateProduct(updated.id, updated),
20
+ * ['products', 'list', { page: 1, pageSize: 20 }],
21
+ * {
22
+ * optimistic: {
23
+ * queryKey: ['products', 'list', { page: 1, pageSize: 20 }],
24
+ * updater: (old, updated) => ({
25
+ * ...old,
26
+ * items: old?.items?.map(p => p.id === updated.id ? updated : p)
27
+ * })
28
+ * },
29
+ * consistency: {
30
+ * mode: 'sync+invalidate',
31
+ * invalidationDelay: 3000
32
+ * },
33
+ * onConflict: (error) => {
34
+ * toast.error('数据冲突,请刷新')
35
+ * }
36
+ * }
37
+ * )
38
+ * ```
39
+ */
40
+ export function useDataGuardMutation(mutationFn, queryKey, options) {
41
+ const queryClient = useQueryClient();
42
+ const { onConflict, onSuccess, optimistic, ...restOptions } = options || {};
43
+ // 增强的 mutation 函数:检测冲突
44
+ const enhancedMutationFn = async (data) => {
45
+ try {
46
+ const result = await mutationFn(data);
47
+ return result;
48
+ }
49
+ catch (error) {
50
+ // 检测冲突错误(409 Conflict)
51
+ if (error && typeof error === 'object' &&
52
+ (error.status === 409 || error.code === "CONFLICT" || error.name === "ConflictError")) {
53
+ onConflict?.(error);
54
+ throw new ConflictError(error);
55
+ }
56
+ throw error;
57
+ }
58
+ };
59
+ // 增强的乐观更新配置
60
+ const enhancedOptimistic = optimistic ? {
61
+ ...optimistic,
62
+ updater: (old, variables) => {
63
+ // 调用原始 updater
64
+ const updated = optimistic.updater(old, variables);
65
+ if (!updated)
66
+ return updated;
67
+ // 检查 variables.id 是否存在
68
+ if (variables.id === undefined || variables.id === null) {
69
+ return updated;
70
+ }
71
+ // 标记最近更新的项
72
+ const withMark = markRecentlyUpdated(updated, variables.id);
73
+ // 乐观更新版本号
74
+ if (old.version !== undefined) {
75
+ withMark.version = old.version + 1;
76
+ }
77
+ // 乐观更新时间戳
78
+ if (old.updatedAt !== undefined) {
79
+ withMark.updatedAt = new Date().toISOString();
80
+ }
81
+ // 更新哈希
82
+ if (old._hash !== undefined) {
83
+ withMark._hash = hashObject(withMark.items);
84
+ }
85
+ return withMark;
86
+ }
87
+ } : undefined;
88
+ // 增强的成功回调
89
+ const enhancedOnSuccess = (data, variables, onMutateResult, context) => {
90
+ // 更新所有家族缓存的元数据
91
+ const familyKey = Array.isArray(queryKey) ? queryKey.slice(0, -1) : [queryKey];
92
+ updateFamilyMetadata(queryClient, familyKey, data);
93
+ // 计算清理延迟时间(基于 invalidationDelay + 缓冲时间)
94
+ const invalidationDelay = options?.consistency?.invalidationDelay || 3000;
95
+ const cleanupDelay = invalidationDelay + 2000; // 在失效后2秒清理
96
+ // 延迟清理最近更新标记
97
+ setTimeout(() => {
98
+ // 检查 variables.id 是否存在
99
+ if (variables.id === undefined || variables.id === null) {
100
+ return;
101
+ }
102
+ const cache = queryClient.getQueryCache();
103
+ const queries = cache.findAll({
104
+ predicate: (q) => startsWithKeyPrefix(q.queryKey, familyKey)
105
+ });
106
+ queries.forEach((q) => {
107
+ const old = queryClient.getQueryData(q.queryKey);
108
+ if (old?._recentlyUpdatedIds) {
109
+ const cleared = clearRecentlyUpdated(old, variables.id);
110
+ queryClient.setQueryData(q.queryKey, cleared);
111
+ }
112
+ });
113
+ }, cleanupDelay);
114
+ // 调用用户的 onSuccess
115
+ onSuccess?.(data, variables, onMutateResult, context);
116
+ };
117
+ return useMutation({
118
+ ...restOptions,
119
+ mutationFn: enhancedMutationFn,
120
+ optimistic: enhancedOptimistic, // 类型复杂,暂时保留
121
+ onSuccess: enhancedOnSuccess,
122
+ onError: (error, variables, onMutateResult, context) => {
123
+ if (error instanceof ConflictError) {
124
+ // 冲突时失效所有家族缓存
125
+ const familyKey = Array.isArray(queryKey) ? queryKey.slice(0, -1) : [queryKey];
126
+ queryClient.invalidateQueries({ queryKey: familyKey });
127
+ }
128
+ // 调用用户的 onError
129
+ restOptions.onError?.(error, variables, onMutateResult, context);
130
+ }
131
+ });
132
+ }
@@ -0,0 +1,28 @@
1
+ import type { QueryKey, UseQueryOptions } from "@tanstack/react-query";
2
+ import type { DataGuardOptions, VersionedEntity, VersionedPaginatedResponse } from "../types/dataGuard.js";
3
+ /**
4
+ * Hook: 创建带数据防护的查询配置
5
+ *
6
+ * 自动根据后端返回的数据选择最佳防护策略:
7
+ * 1. 如果有 version 字段 → 使用版本号比较(最可靠)
8
+ * 2. 如果有 updatedAt 字段 → 使用时间戳比较(次优)
9
+ * 3. 都没有 → 使用内容哈希比较(兜底)
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const { data } = useEnhancedQuery(
14
+ * useDataGuardQueryConfig(
15
+ * ['products', 'list', { page: 1, pageSize: 20 }],
16
+ * () => fetchProducts(1, 20),
17
+ * {
18
+ * maxDataAge: 5000,
19
+ * onStaleDataDetected: (info) => {
20
+ * console.warn('检测到旧数据', info)
21
+ * }
22
+ * }
23
+ * )
24
+ * )
25
+ * ```
26
+ */
27
+ export declare function useDataGuardQueryConfig<T extends VersionedEntity>(queryKey: QueryKey, fetchFn: () => Promise<VersionedPaginatedResponse<T>>, options?: DataGuardOptions): Pick<UseQueryOptions<VersionedPaginatedResponse<T>>, "queryKey" | "queryFn">;
28
+ //# sourceMappingURL=useDataGuardQuery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDataGuardQuery.d.ts","sourceRoot":"","sources":["../../src/hooks/useDataGuardQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAK3G;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,eAAe,EAC/D,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,EACrD,OAAO,CAAC,EAAE,gBAAgB,GACzB,IAAI,CAAC,eAAe,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC,CAwD9E"}
@@ -0,0 +1,71 @@
1
+ import { useQueryClient } from "@tanstack/react-query";
2
+ import { applyDataGuard, addHashToData } from "../utils/dataGuard.js";
3
+ import { isDev } from "../core/env.js";
4
+ /**
5
+ * Hook: 创建带数据防护的查询配置
6
+ *
7
+ * 自动根据后端返回的数据选择最佳防护策略:
8
+ * 1. 如果有 version 字段 → 使用版本号比较(最可靠)
9
+ * 2. 如果有 updatedAt 字段 → 使用时间戳比较(次优)
10
+ * 3. 都没有 → 使用内容哈希比较(兜底)
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const { data } = useEnhancedQuery(
15
+ * useDataGuardQueryConfig(
16
+ * ['products', 'list', { page: 1, pageSize: 20 }],
17
+ * () => fetchProducts(1, 20),
18
+ * {
19
+ * maxDataAge: 5000,
20
+ * onStaleDataDetected: (info) => {
21
+ * console.warn('检测到旧数据', info)
22
+ * }
23
+ * }
24
+ * )
25
+ * )
26
+ * ```
27
+ */
28
+ export function useDataGuardQueryConfig(queryKey, fetchFn, options) {
29
+ const queryClient = useQueryClient();
30
+ return {
31
+ queryKey,
32
+ queryFn: async () => {
33
+ const newData = await fetchFn();
34
+ const cached = queryClient.getQueryData(queryKey);
35
+ // 应用数据防护
36
+ const { shouldReject, reason, strategy, guardDetails } = applyDataGuard(newData, cached, queryKey, options);
37
+ // 触发回调
38
+ options?.onDataGuardApplied?.({
39
+ strategy,
40
+ passed: !shouldReject,
41
+ details: guardDetails
42
+ });
43
+ if (shouldReject) {
44
+ if (isDev) {
45
+ console.warn("[Data Guard] 拒绝旧数据", {
46
+ reason,
47
+ strategy,
48
+ queryKey,
49
+ guardDetails
50
+ });
51
+ }
52
+ options?.onStaleDataDetected?.({
53
+ reason,
54
+ strategy,
55
+ queryKey,
56
+ cached,
57
+ rejected: newData
58
+ });
59
+ return cached;
60
+ }
61
+ if (isDev) {
62
+ console.log("[Data Guard] 接受新数据", {
63
+ strategy,
64
+ guardDetails
65
+ });
66
+ }
67
+ // 添加哈希标记
68
+ return addHashToData(newData);
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,69 @@
1
+ import type { QueryKey } from "@tanstack/react-query";
2
+ /**
3
+ * 数据防护策略类型
4
+ */
5
+ export type DataGuardStrategy = "version" | "timestamp" | "hash" | "none";
6
+ /**
7
+ * 带版本控制的实体基础接口
8
+ */
9
+ export interface VersionedEntity {
10
+ id: string | number;
11
+ version?: number;
12
+ updatedAt?: string;
13
+ }
14
+ /**
15
+ * 带版本控制的分页响应
16
+ */
17
+ export interface VersionedPaginatedResponse<T> {
18
+ items: T[];
19
+ total: number;
20
+ page?: number;
21
+ pageSize?: number;
22
+ version?: number;
23
+ updatedAt?: string;
24
+ _hash?: string;
25
+ _recentlyUpdatedIds?: Set<string | number>;
26
+ }
27
+ /**
28
+ * 旧数据检测信息
29
+ */
30
+ export interface StaleDataInfo {
31
+ reason: string;
32
+ strategy: DataGuardStrategy;
33
+ queryKey: QueryKey;
34
+ cached: any;
35
+ rejected: any;
36
+ }
37
+ /**
38
+ * 数据防护应用信息
39
+ */
40
+ export interface DataGuardInfo {
41
+ strategy: DataGuardStrategy;
42
+ passed: boolean;
43
+ details: any;
44
+ }
45
+ /**
46
+ * 数据防护配置选项
47
+ */
48
+ export interface DataGuardOptions {
49
+ /** 时间戳模式下,最大接受的数据年龄(毫秒),默认 10000 */
50
+ maxDataAge?: number;
51
+ /** 启用版本号检查,默认 true */
52
+ enableVersionCheck?: boolean;
53
+ /** 启用时间戳检查,默认 true */
54
+ enableTimestampCheck?: boolean;
55
+ /** 启用哈希检查(兜底),默认 true */
56
+ enableHashCheck?: boolean;
57
+ /** 检测到旧数据时的回调 */
58
+ onStaleDataDetected?: (info: StaleDataInfo) => void;
59
+ /** 应用防护时的回调 */
60
+ onDataGuardApplied?: (info: DataGuardInfo) => void;
61
+ }
62
+ /**
63
+ * 冲突错误类
64
+ */
65
+ export declare class ConflictError extends Error {
66
+ details: any;
67
+ constructor(details: any);
68
+ }
69
+ //# sourceMappingURL=dataGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataGuard.d.ts","sourceRoot":"","sources":["../../src/types/dataGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B,CAAC,CAAC;IAC3C,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,GAAG,CAAC;IACZ,QAAQ,EAAE,GAAG,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,GAAG,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,sBAAsB;IACtB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,yBAAyB;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB;IACjB,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IACpD,eAAe;IACf,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;CACpD;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACnB,OAAO,EAAE,GAAG;gBAAZ,OAAO,EAAE,GAAG;CAIhC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 冲突错误类
3
+ */
4
+ export class ConflictError extends Error {
5
+ constructor(details) {
6
+ super("Data conflict detected");
7
+ this.details = details;
8
+ this.name = "ConflictError";
9
+ }
10
+ }
@@ -1,5 +1,6 @@
1
1
  import type { QueryKey, UseMutationOptions } from "@tanstack/react-query";
2
2
  export * from "./base.js";
3
+ export * from "./dataGuard.js";
3
4
  export * from "./infinite.js";
4
5
  export * from "./offline.js";
5
6
  export * from "./optimistic.js";
@@ -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,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;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,4 +1,5 @@
1
1
  export * from "./base.js";
2
+ export * from "./dataGuard.js";
2
3
  export * from "./infinite.js";
3
4
  export * from "./offline.js";
4
5
  export * from "./optimistic.js";
@@ -0,0 +1,37 @@
1
+ import type { QueryClient, QueryKey } from "@tanstack/react-query";
2
+ import type { DataGuardOptions, DataGuardStrategy, VersionedEntity, VersionedPaginatedResponse } from "../types/dataGuard.js";
3
+ /**
4
+ * 计算对象的哈希值(键排序后计算,确保一致性)
5
+ */
6
+ export declare function hashObject(obj: any): string;
7
+ /**
8
+ * 自适应数据防护:根据数据特征自动选择最佳防护策略
9
+ *
10
+ * 策略优先级:
11
+ * 1. version(版本号)- 最可靠
12
+ * 2. updatedAt(时间戳)- 次优
13
+ * 3. hash(内容哈希)- 兜底
14
+ */
15
+ export declare function applyDataGuard<T extends VersionedEntity>(newData: VersionedPaginatedResponse<T>, cached: VersionedPaginatedResponse<T> | undefined, queryKey: QueryKey, options?: DataGuardOptions): {
16
+ shouldReject: boolean;
17
+ reason: string;
18
+ strategy: DataGuardStrategy;
19
+ guardDetails: any;
20
+ };
21
+ /**
22
+ * 为查询数据添加哈希标记
23
+ */
24
+ export declare function addHashToData<T extends VersionedEntity>(data: VersionedPaginatedResponse<T>): VersionedPaginatedResponse<T>;
25
+ /**
26
+ * 标记最近更新的项
27
+ */
28
+ export declare function markRecentlyUpdated<T extends VersionedEntity>(data: VersionedPaginatedResponse<T>, updatedId: string | number): VersionedPaginatedResponse<T>;
29
+ /**
30
+ * 清理最近更新的标记
31
+ */
32
+ export declare function clearRecentlyUpdated<T extends VersionedEntity>(data: VersionedPaginatedResponse<T>, updatedId: string | number): VersionedPaginatedResponse<T>;
33
+ /**
34
+ * 更新所有家族缓存的元数据(版本号、时间戳、哈希)
35
+ */
36
+ export declare function updateFamilyMetadata<T extends VersionedEntity>(queryClient: QueryClient, familyKey: QueryKey, response: Partial<VersionedPaginatedResponse<T>>): void;
37
+ //# sourceMappingURL=dataGuard.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,243 @@
1
+ import { startsWithKeyPrefix } from "./consistency.js";
2
+ /**
3
+ * 简单的字符串哈希函数(DJB2算法变体)
4
+ */
5
+ function simpleHash(str) {
6
+ let hash = 0;
7
+ for (let i = 0; i < str.length; i++) {
8
+ const char = str.charCodeAt(i);
9
+ hash = ((hash << 5) - hash) + char;
10
+ hash = hash | 0; // 转换为32位整数
11
+ }
12
+ return Math.abs(hash).toString(36);
13
+ }
14
+ /**
15
+ * 对象键排序(确保一致的哈希值)
16
+ */
17
+ function sortObjectKeys(obj) {
18
+ if (obj === null || obj === undefined)
19
+ return obj;
20
+ if (typeof obj !== 'object')
21
+ return obj;
22
+ if (Array.isArray(obj))
23
+ return obj.map(sortObjectKeys);
24
+ const sorted = {};
25
+ Object.keys(obj).sort().forEach(key => {
26
+ sorted[key] = sortObjectKeys(obj[key]);
27
+ });
28
+ return sorted;
29
+ }
30
+ /**
31
+ * 计算对象的哈希值(键排序后计算,确保一致性)
32
+ */
33
+ export function hashObject(obj) {
34
+ try {
35
+ const sorted = sortObjectKeys(obj);
36
+ return simpleHash(JSON.stringify(sorted));
37
+ }
38
+ catch {
39
+ return simpleHash(String(obj));
40
+ }
41
+ }
42
+ /**
43
+ * 自适应数据防护:根据数据特征自动选择最佳防护策略
44
+ *
45
+ * 策略优先级:
46
+ * 1. version(版本号)- 最可靠
47
+ * 2. updatedAt(时间戳)- 次优
48
+ * 3. hash(内容哈希)- 兜底
49
+ */
50
+ export function applyDataGuard(newData, cached, queryKey, options = {}) {
51
+ const { maxDataAge = 10000, enableVersionCheck = true, enableTimestampCheck = true, enableHashCheck = true } = options;
52
+ // 如果没有缓存,接受新数据
53
+ if (!cached) {
54
+ return {
55
+ shouldReject: false,
56
+ reason: "No cached data",
57
+ strategy: "none",
58
+ guardDetails: {}
59
+ };
60
+ }
61
+ let shouldReject = false;
62
+ let reason = "";
63
+ let strategy = "none";
64
+ let guardDetails = {};
65
+ // ============================================
66
+ // 策略1:版本号比较(最可靠)
67
+ // ============================================
68
+ if (enableVersionCheck && cached.version !== undefined && newData.version !== undefined) {
69
+ strategy = "version";
70
+ if (newData.version < cached.version) {
71
+ shouldReject = true;
72
+ reason = `版本号回退:缓存版本 ${cached.version} > 新数据版本 ${newData.version}`;
73
+ guardDetails = {
74
+ cachedVersion: cached.version,
75
+ newVersion: newData.version,
76
+ diff: cached.version - newData.version
77
+ };
78
+ }
79
+ else if (newData.version === cached.version) {
80
+ guardDetails = {
81
+ version: newData.version,
82
+ status: "identical"
83
+ };
84
+ }
85
+ else {
86
+ guardDetails = {
87
+ cachedVersion: cached.version,
88
+ newVersion: newData.version,
89
+ status: "updated"
90
+ };
91
+ }
92
+ }
93
+ // ============================================
94
+ // 策略2:时间戳比较(次优)
95
+ // ============================================
96
+ else if (enableTimestampCheck && cached.updatedAt && newData.updatedAt) {
97
+ strategy = "timestamp";
98
+ try {
99
+ const cachedTime = new Date(cached.updatedAt).getTime();
100
+ const newTime = new Date(newData.updatedAt).getTime();
101
+ const timeDiff = cachedTime - newTime;
102
+ const dataAge = Date.now() - newTime;
103
+ guardDetails = {
104
+ cachedTime: cached.updatedAt,
105
+ newTime: newData.updatedAt,
106
+ timeDiff,
107
+ dataAge
108
+ };
109
+ // 新数据的时间戳更旧
110
+ if (newTime < cachedTime) {
111
+ // 如果数据年龄超过阈值,拒绝
112
+ if (dataAge > maxDataAge) {
113
+ shouldReject = true;
114
+ reason = `时间戳过期:新数据时间 ${newData.updatedAt} 早于缓存 ${cached.updatedAt},且数据年龄 ${dataAge}ms 超过阈值 ${maxDataAge}ms`;
115
+ }
116
+ else {
117
+ // 数据年龄在可接受范围内,记录警告但接受
118
+ guardDetails.warning = `时间戳略旧但在可接受范围内(${dataAge}ms < ${maxDataAge}ms)`;
119
+ }
120
+ }
121
+ }
122
+ catch (error) {
123
+ guardDetails.error = `时间戳解析失败: ${error}`;
124
+ // 时间戳解析失败,不使用时间戳策略
125
+ // 将在下一个 else if 中尝试 hash 策略
126
+ }
127
+ }
128
+ // ============================================
129
+ // 策略3:内容哈希比较(兜底)
130
+ // ============================================
131
+ else if (enableHashCheck) {
132
+ strategy = "hash";
133
+ const newHash = hashObject(newData.items);
134
+ const cachedHash = cached._hash;
135
+ guardDetails = {
136
+ cachedHash,
137
+ newHash,
138
+ identical: cachedHash === newHash
139
+ };
140
+ if (cachedHash && cachedHash === newHash) {
141
+ // 内容完全相同,不需要更新
142
+ shouldReject = true;
143
+ reason = "内容哈希相同,数据未变化";
144
+ }
145
+ else if (cachedHash && cached._recentlyUpdatedIds && cached._recentlyUpdatedIds.size > 0) {
146
+ // 检查最近更新的项是否被回退
147
+ let hasRevert = false;
148
+ for (const id of cached._recentlyUpdatedIds) {
149
+ const cachedItem = cached.items.find(item => item.id === id);
150
+ const newItem = newData.items.find(item => item.id === id);
151
+ if (cachedItem && newItem) {
152
+ const cachedItemHash = hashObject(cachedItem);
153
+ const newItemHash = hashObject(newItem);
154
+ if (cachedItemHash !== newItemHash) {
155
+ // 简单启发式:如果缓存项有更多字段,可能是新的
156
+ if (Object.keys(cachedItem).length > Object.keys(newItem).length) {
157
+ hasRevert = true;
158
+ guardDetails.revertedItem = { id, cachedItem, newItem };
159
+ break;
160
+ }
161
+ }
162
+ }
163
+ }
164
+ if (hasRevert) {
165
+ shouldReject = true;
166
+ reason = "检测到最近更新的项可能被回退";
167
+ }
168
+ }
169
+ }
170
+ return {
171
+ shouldReject,
172
+ reason,
173
+ strategy,
174
+ guardDetails
175
+ };
176
+ }
177
+ /**
178
+ * 为查询数据添加哈希标记
179
+ */
180
+ export function addHashToData(data) {
181
+ return {
182
+ ...data,
183
+ _hash: hashObject(data.items)
184
+ };
185
+ }
186
+ /**
187
+ * 标记最近更新的项
188
+ */
189
+ export function markRecentlyUpdated(data, updatedId) {
190
+ const recentlyUpdatedIds = new Set(data._recentlyUpdatedIds || []);
191
+ recentlyUpdatedIds.add(updatedId);
192
+ return {
193
+ ...data,
194
+ _recentlyUpdatedIds: recentlyUpdatedIds
195
+ };
196
+ }
197
+ /**
198
+ * 清理最近更新的标记
199
+ */
200
+ export function clearRecentlyUpdated(data, updatedId) {
201
+ if (!data._recentlyUpdatedIds)
202
+ return data;
203
+ const recentlyUpdatedIds = new Set(data._recentlyUpdatedIds);
204
+ recentlyUpdatedIds.delete(updatedId);
205
+ return {
206
+ ...data,
207
+ _recentlyUpdatedIds: recentlyUpdatedIds.size > 0 ? recentlyUpdatedIds : undefined
208
+ };
209
+ }
210
+ /**
211
+ * 更新所有家族缓存的元数据(版本号、时间戳、哈希)
212
+ */
213
+ export function updateFamilyMetadata(queryClient, familyKey, response) {
214
+ const cache = queryClient.getQueryCache();
215
+ const queries = cache.findAll({
216
+ predicate: (q) => startsWithKeyPrefix(q.queryKey, familyKey)
217
+ });
218
+ queries.forEach((q) => {
219
+ const old = queryClient.getQueryData(q.queryKey);
220
+ if (old) {
221
+ const updated = { ...old };
222
+ // 更新版本号
223
+ if (response.version !== undefined) {
224
+ updated.version = response.version;
225
+ }
226
+ else if (old.version !== undefined) {
227
+ updated.version = old.version + 1;
228
+ }
229
+ // 更新时间戳
230
+ if (response.updatedAt) {
231
+ updated.updatedAt = response.updatedAt;
232
+ }
233
+ else if (old.updatedAt !== undefined) {
234
+ updated.updatedAt = new Date().toISOString();
235
+ }
236
+ // 更新哈希
237
+ if (old._hash !== undefined) {
238
+ updated._hash = hashObject(updated.items);
239
+ }
240
+ queryClient.setQueryData(q.queryKey, updated);
241
+ }
242
+ });
243
+ }
@@ -7,4 +7,5 @@ export { createQueryKeyFactory, createSimpleQueryKeyFactory, extractParamsFromKe
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
9
  export { startsWithKeyPrefix, syncEntityAcrossFamily, DEFAULT_FAMILY_SYNC, type FamilySyncConfig } from "./consistency.js";
10
+ export { applyDataGuard, addHashToData, hashObject, markRecentlyUpdated, clearRecentlyUpdated, updateFamilyMetadata } from "./dataGuard.js";
10
11
  //# 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"}
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"}
@@ -7,3 +7,4 @@ export { createQueryKeyFactory, createSimpleQueryKeyFactory, extractParamsFromKe
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
9
  export { startsWithKeyPrefix, syncEntityAcrossFamily, DEFAULT_FAMILY_SYNC } from "./consistency.js";
10
+ export { applyDataGuard, addHashToData, hashObject, markRecentlyUpdated, clearRecentlyUpdated, updateFamilyMetadata } from "./dataGuard.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qiaopeng/tanstack-query-plus",
3
- "version": "0.3.1",
4
- "description": "Enhanced TanStack Query toolkit: defaults, hooks, persistence, offline, utils",
3
+ "version": "0.4.0",
4
+ "description": "Enhanced TanStack Query toolkit: defaults, hooks, persistence, offline, data guard, utils",
5
5
  "author": "qiaopeng",
6
6
  "license": "MIT",
7
7
  "repository": {