Skip to content

Commit 6008d4d

Browse files
committed
refactor: implemented the core infrastructure and QueryClient foundation
1 parent 20e0698 commit 6008d4d

File tree

10 files changed

+1602
-7
lines changed

10 files changed

+1602
-7
lines changed

_internal/composable/useCache.ts

Lines changed: 146 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,155 @@
11
import { Cache } from '../types'
22
import { serializeFunc, sortObject, stringifyQuery } from '../utils/common'
33

4-
export function useCache(fetcher: (params: object) => Promise<any>, provider: Cache) {
4+
interface CacheOptions {
5+
maxSize?: number // Maximum number of items in cache
6+
ttl?: number // Time-to-live in milliseconds
7+
maxAge?: number // Maximum age of cached items in milliseconds
8+
updateAgeOnGet?: boolean // Update item age when accessed
9+
}
10+
11+
interface CacheItem<T> {
12+
value: T
13+
timestamp: number
14+
lastAccessed: number
15+
}
16+
17+
interface CacheStats {
18+
hits: number
19+
misses: number
20+
size: number
21+
}
22+
23+
export function useCache<T = any>(
24+
fetcher: (params: object) => Promise<T>,
25+
provider: Cache,
26+
options: CacheOptions = {}
27+
) {
28+
const {
29+
maxSize = 1000,
30+
ttl = 5 * 60 * 1000, // 5 minutes default TTL
31+
maxAge = 30 * 60 * 1000, // 30 minutes default max age
32+
updateAgeOnGet = true
33+
} = options
34+
535
const baseKey = serializeFunc(fetcher)
6-
function formatString(key) {
36+
const stats: CacheStats = {
37+
hits: 0,
38+
misses: 0,
39+
size: 0
40+
}
41+
42+
// LRU tracking
43+
const keysByAge = new Map<string, number>()
44+
45+
function formatString(key: unknown): string {
746
return `${baseKey}@${stringifyQuery(sortObject(key))}`
847
}
48+
49+
function isExpired(item: CacheItem<T>): boolean {
50+
const now = Date.now()
51+
const age = now - item.timestamp
52+
const lastAccessedAge = now - item.lastAccessed
53+
54+
return age > maxAge || lastAccessedAge > ttl
55+
}
56+
57+
function cleanup(): void {
58+
if (keysByAge.size <= maxSize) return
59+
60+
// Remove oldest items until we're under maxSize
61+
const sortedKeys = Array.from(keysByAge.entries())
62+
.sort(([, a], [, b]) => a - b)
63+
.slice(0, keysByAge.size - maxSize)
64+
65+
for (const [key] of sortedKeys) {
66+
provider.delete(key)
67+
keysByAge.delete(key)
68+
}
69+
70+
stats.size = keysByAge.size
71+
}
72+
73+
function updateAccessTime(key: string): void {
74+
if (updateAgeOnGet) {
75+
const item = provider.get(key) as CacheItem<T> | undefined
76+
if (item) {
77+
item.lastAccessed = Date.now()
78+
provider.set(key, item)
79+
keysByAge.set(key, item.lastAccessed)
80+
}
81+
}
82+
}
83+
984
return {
10-
set: (key, value) => provider.set(formatString(key), value),
11-
get: (key) => (provider.get(formatString(key)) ? provider.get(formatString(key)) : undefined),
12-
delete: (key) => provider.delete(formatString(key)),
13-
cached: (key) => provider.get(formatString(key)) !== undefined,
85+
set: (key: unknown, value: T): void => {
86+
const formattedKey = formatString(key)
87+
const cacheItem: CacheItem<T> = {
88+
value,
89+
timestamp: Date.now(),
90+
lastAccessed: Date.now()
91+
}
92+
93+
cleanup()
94+
provider.set(formattedKey, cacheItem)
95+
keysByAge.set(formattedKey, cacheItem.timestamp)
96+
stats.size = keysByAge.size
97+
},
98+
99+
get: (key: unknown): T | undefined => {
100+
const formattedKey = formatString(key)
101+
const item = provider.get(formattedKey) as CacheItem<T> | undefined
102+
103+
if (!item) {
104+
stats.misses++
105+
return undefined
106+
}
107+
108+
if (isExpired(item)) {
109+
this.delete(key)
110+
stats.misses++
111+
return undefined
112+
}
113+
114+
updateAccessTime(formattedKey)
115+
stats.hits++
116+
return item.value
117+
},
118+
119+
delete: (key: unknown): void => {
120+
const formattedKey = formatString(key)
121+
provider.delete(formattedKey)
122+
keysByAge.delete(formattedKey)
123+
stats.size = keysByAge.size
124+
},
125+
126+
cached: (key: unknown): boolean => {
127+
const formattedKey = formatString(key)
128+
const item = provider.get(formattedKey) as CacheItem<T> | undefined
129+
return item !== undefined && !isExpired(item)
130+
},
131+
132+
clear: (): void => {
133+
// Clear all cache entries
134+
for (const [key] of keysByAge) {
135+
provider.delete(key)
136+
}
137+
keysByAge.clear()
138+
stats.size = 0
139+
},
140+
141+
getStats: (): CacheStats => ({
142+
...stats
143+
}),
144+
145+
prune: (): void => {
146+
// Remove all expired items
147+
for (const [key] of keysByAge) {
148+
const item = provider.get(key) as CacheItem<T> | undefined
149+
if (item && isExpired(item)) {
150+
this.delete(key)
151+
}
152+
}
153+
}
14154
}
15155
}

core/conditions-key-manager.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { QueryKey, QueryFilters } from './query-types'
2+
import { stringifyQuery, sortObject, isEquivalent } from 'vue-condition-watcher/_internal'
3+
4+
/**
5+
* ConditionsKeyManager - Manages conditions-based query key serialization and matching
6+
* Bridges vue-condition-watcher's conditions API with TanStack Query's queryKey system
7+
*/
8+
export class ConditionsKeyManager {
9+
private static readonly CONDITIONS_PREFIX = 'conditions'
10+
11+
/**
12+
* Serialize conditions for caching (primary method)
13+
* Converts conditions object to a stable string representation
14+
*/
15+
serializeConditions(conditions: Record<string, any>): string {
16+
if (!conditions || typeof conditions !== 'object') {
17+
return ''
18+
}
19+
20+
// Sort object keys for consistent serialization
21+
const sortedConditions = sortObject(conditions)
22+
return stringifyQuery(sortedConditions)
23+
}
24+
25+
/**
26+
* Convert conditions to TanStack Query key format (internal use)
27+
* Creates a QueryKey array that includes conditions data
28+
*/
29+
conditionsToQueryKey(conditions: Record<string, any>): QueryKey {
30+
if (!conditions || typeof conditions !== 'object') {
31+
return [ConditionsKeyManager.CONDITIONS_PREFIX]
32+
}
33+
34+
// Create a stable query key that includes the conditions
35+
const sortedConditions = sortObject(conditions)
36+
return [ConditionsKeyManager.CONDITIONS_PREFIX, sortedConditions]
37+
}
38+
39+
/**
40+
* Extract conditions from a QueryKey if it was created from conditions
41+
*/
42+
queryKeyToConditions(queryKey: QueryKey): Record<string, any> | null {
43+
if (!Array.isArray(queryKey) || queryKey.length < 2) {
44+
return null
45+
}
46+
47+
if (queryKey[0] !== ConditionsKeyManager.CONDITIONS_PREFIX) {
48+
return null
49+
}
50+
51+
const conditions = queryKey[1]
52+
if (typeof conditions === 'object' && conditions !== null) {
53+
return conditions as Record<string, any>
54+
}
55+
56+
return null
57+
}
58+
59+
/**
60+
* Match conditions for invalidation and filtering
61+
* Supports partial matching for flexible query invalidation
62+
*/
63+
matchConditions(queryKey: QueryKey, conditionsPattern: Record<string, any>): boolean {
64+
const queryConditions = this.queryKeyToConditions(queryKey)
65+
66+
if (!queryConditions) {
67+
return false
68+
}
69+
70+
// If no pattern provided, match all conditions-based queries
71+
if (!conditionsPattern || Object.keys(conditionsPattern).length === 0) {
72+
return true
73+
}
74+
75+
// Check if all pattern properties match the query conditions
76+
return this.isPartialMatch(queryConditions, conditionsPattern)
77+
}
78+
79+
/**
80+
* Hash conditions for efficient lookups
81+
* Creates a stable hash from conditions for Map keys
82+
*/
83+
hashConditions(conditions: Record<string, any>): string {
84+
const serialized = this.serializeConditions(conditions)
85+
return this.simpleHash(serialized)
86+
}
87+
88+
/**
89+
* Check if two conditions objects are equivalent
90+
*/
91+
areConditionsEquivalent(a: Record<string, any>, b: Record<string, any>): boolean {
92+
return isEquivalent(a, b)
93+
}
94+
95+
/**
96+
* Create a query key from either conditions or explicit queryKey
97+
* Provides flexibility for both APIs
98+
*/
99+
createQueryKey(options: {
100+
queryKey?: QueryKey
101+
conditions?: Record<string, any>
102+
}): QueryKey {
103+
if (options.queryKey) {
104+
return options.queryKey
105+
}
106+
107+
if (options.conditions) {
108+
return this.conditionsToQueryKey(options.conditions)
109+
}
110+
111+
// Fallback to a default key
112+
return [ConditionsKeyManager.CONDITIONS_PREFIX, {}]
113+
}
114+
115+
/**
116+
* Create filters for finding queries by conditions
117+
*/
118+
createFiltersFromConditions(conditionsPattern?: Record<string, any>): QueryFilters {
119+
if (!conditionsPattern) {
120+
return {
121+
predicate: (query) => this.queryKeyToConditions(query.queryKey) !== null
122+
}
123+
}
124+
125+
return {
126+
predicate: (query) => this.matchConditions(query.queryKey, conditionsPattern)
127+
}
128+
}
129+
130+
/**
131+
* Validate that conditions are serializable
132+
*/
133+
validateConditions(conditions: Record<string, any>): boolean {
134+
try {
135+
// Check for non-serializable values like functions
136+
const hasNonSerializableValues = this.hasNonSerializableValues(conditions)
137+
if (hasNonSerializableValues) {
138+
return false
139+
}
140+
141+
this.serializeConditions(conditions)
142+
return true
143+
} catch {
144+
return false
145+
}
146+
}
147+
148+
// Private helper methods
149+
150+
/**
151+
* Check if target object contains all properties from pattern with matching values
152+
*/
153+
private isPartialMatch(target: Record<string, any>, pattern: Record<string, any>): boolean {
154+
for (const [key, value] of Object.entries(pattern)) {
155+
if (!(key in target)) {
156+
return false
157+
}
158+
159+
// Handle nested objects
160+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
161+
if (typeof target[key] !== 'object' || target[key] === null || Array.isArray(target[key])) {
162+
return false
163+
}
164+
if (!this.isPartialMatch(target[key], value)) {
165+
return false
166+
}
167+
} else {
168+
// Use deep equality check for arrays and primitive values
169+
if (!isEquivalent(target[key], value)) {
170+
return false
171+
}
172+
}
173+
}
174+
175+
return true
176+
}
177+
178+
/**
179+
* Check if object contains non-serializable values like functions
180+
*/
181+
private hasNonSerializableValues(obj: Record<string, any>): boolean {
182+
for (const [, value] of Object.entries(obj)) {
183+
if (typeof value === 'function') {
184+
return true
185+
}
186+
187+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
188+
if (this.hasNonSerializableValues(value)) {
189+
return true
190+
}
191+
}
192+
193+
if (Array.isArray(value)) {
194+
for (const item of value) {
195+
if (typeof item === 'function') {
196+
return true
197+
}
198+
if (typeof item === 'object' && item !== null && this.hasNonSerializableValues(item)) {
199+
return true
200+
}
201+
}
202+
}
203+
}
204+
205+
return false
206+
}
207+
208+
/**
209+
* Simple hash function for string input
210+
*/
211+
private simpleHash(str: string): string {
212+
let hash = 0
213+
if (str.length === 0) return hash.toString()
214+
215+
for (let i = 0; i < str.length; i++) {
216+
const char = str.charCodeAt(i)
217+
hash = ((hash << 5) - hash) + char
218+
hash = hash & hash // Convert to 32-bit integer
219+
}
220+
221+
return Math.abs(hash).toString(36)
222+
}
223+
}

0 commit comments

Comments
 (0)