Skip to content

Commit b768948

Browse files
authored
Merge pull request #1 from muzix/refactor/improve-query-keey
refactor: improve query key
2 parents 30423e3 + 996e616 commit b768948

8 files changed

Lines changed: 515 additions & 89 deletions

File tree

Sources/SwiftUIQuery/Core/QueryKey.swift

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,85 @@ import Foundation
44

55
/// A protocol that represents a unique identifier for queries
66
/// Equivalent to TanStack Query's QueryKey (ReadonlyArray<unknown>)
7-
public protocol QueryKey: Sendable, Hashable, Codable {
7+
public protocol QueryKey: Sendable, Equatable {
88
/// Convert the query key to a string hash for identification
99
var queryHash: String { get }
1010
}
1111

12-
/// Default QueryKey implementation using arrays of strings
13-
public struct ArrayQueryKey: QueryKey {
14-
public let components: [String]
15-
16-
public init(_ components: String...) {
17-
self.components = components
12+
extension QueryKey where Self: Hashable & Codable {
13+
public var queryHash: String {
14+
let jsonEncoder = JSONEncoder()
15+
jsonEncoder.outputFormatting = .sortedKeys // stable output with key sorted
16+
guard let jsonData = try? jsonEncoder.encode(self) else {
17+
return "\(hashValue)"
18+
}
19+
return String(decoding: jsonData, as: UTF8.self)
1820
}
21+
}
1922

20-
public init(_ components: [String]) {
21-
self.components = components
22-
}
23+
// MARK: - QueryKey Extensions for Common Types
2324

25+
extension String: QueryKey {
2426
public var queryHash: String {
25-
// Create a deterministic hash similar to TanStack Query's approach
26-
guard let jsonData = try? JSONEncoder().encode(components.sorted()),
27-
let jsonString = String(data: jsonData, encoding: .utf8) else {
28-
return components.sorted().joined(separator: "|")
29-
}
30-
return jsonString
27+
self
3128
}
3229
}
3330

34-
/// Generic QueryKey implementation for any Codable type
35-
public struct GenericQueryKey<T: Sendable & Codable & Hashable>: QueryKey {
36-
public let value: T
31+
extension Array: QueryKey where Element: Hashable & Codable {}
32+
extension Dictionary: QueryKey where Key: Hashable & Codable, Value: Hashable & Codable {}
3733

38-
public init(_ value: T) {
39-
self.value = value
34+
public typealias QueryKeyCodable = Codable & Hashable & Sendable
35+
public struct KeyTuple2<K1: QueryKeyCodable, K2: QueryKeyCodable>: QueryKey, QueryKeyCodable {
36+
public let key1: K1
37+
public let key2: K2
38+
39+
public init(_ key1: K1, _ key2: K2) {
40+
self.key1 = key1
41+
self.key2 = key2
4042
}
4143

42-
public var queryHash: String {
43-
guard let jsonData = try? JSONEncoder().encode(value),
44-
let jsonString = String(data: jsonData, encoding: .utf8) else {
45-
return String(describing: value)
46-
}
47-
return jsonString
44+
public init(_ key1: (some Any).Type, _ key2: K2) where K1 == String {
45+
self.key1 = String(describing: key1)
46+
self.key2 = key2
4847
}
4948
}
5049

51-
// MARK: - QueryKey Extensions for Common Types
50+
public struct KeyTuple3<K1: QueryKeyCodable, K2: QueryKeyCodable, K3: QueryKeyCodable>: QueryKey, QueryKeyCodable {
51+
public let key1: K1
52+
public let key2: K2
53+
public let key3: K3
5254

53-
extension String: QueryKey {
54-
public var queryHash: String {
55-
self
55+
public init(_ key1: K1, _ key2: K2, _ key3: K3) {
56+
self.key1 = key1
57+
self.key2 = key2
58+
self.key3 = key3
59+
}
60+
61+
public init(_ key1: (some Any).Type, _ key2: K2, _ key3: K3) where K1 == String {
62+
self.key1 = String(describing: key1)
63+
self.key2 = key2
64+
self.key3 = key3
5665
}
5766
}
5867

59-
extension [String]: QueryKey {
60-
public var queryHash: String {
61-
// Create a deterministic hash by joining sorted components
62-
sorted().joined(separator: "|")
68+
public struct KeyTuple4<K1: QueryKeyCodable, K2: QueryKeyCodable, K3: QueryKeyCodable, K4: QueryKeyCodable>: QueryKey,
69+
QueryKeyCodable {
70+
public let key1: K1
71+
public let key2: K2
72+
public let key3: K3
73+
public let key4: K4
74+
75+
public init(_ key1: K1, _ key2: K2, _ key3: K3, _ key4: K4) {
76+
self.key1 = key1
77+
self.key2 = key2
78+
self.key3 = key3
79+
self.key4 = key4
80+
}
81+
82+
public init(_ key1: (some Any).Type, _ key2: K2, _ key3: K3, _ key4: K4) where K1 == String {
83+
self.key1 = String(describing: key1)
84+
self.key2 = key2
85+
self.key3 = key3
86+
self.key4 = key4
6387
}
6488
}

Sources/SwiftUIQuery/UseInfiniteQuery.swift

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,9 @@ public struct UseInfiniteQuery<
304304

305305
/// Additional convenience methods for SwiftUI integration
306306
extension UseInfiniteQuery {
307-
/// Create a UseInfiniteQuery with string-based query key
307+
/// Create UseInfiniteQuery with KeyTuple2-based query key
308308
/// - Parameters:
309-
/// - queryKey: String-based query key
309+
/// - queryKey: KeyTuple2 identifier for the query
310310
/// - queryFn: Function that fetches page data
311311
/// - getNextPageParam: Function to get next page parameter from pages
312312
/// - getPreviousPageParam: Function to determine the previous page parameter
@@ -323,9 +323,135 @@ extension UseInfiniteQuery {
323323
/// - enabled: Whether the query should execute automatically (default: true)
324324
/// - queryClient: Optional query client (uses shared instance if nil)
325325
/// - content: View builder that receives the query result
326-
public init(
327-
queryKey: String,
328-
queryFn: @escaping @Sendable (String, TPageParam?) async throws -> TData,
326+
public init<K1: QueryKeyCodable, K2: QueryKeyCodable>(
327+
queryKey: KeyTuple2<K1, K2>,
328+
queryFn: @escaping @Sendable (KeyTuple2<K1, K2>, TPageParam?) async throws -> TData,
329+
getNextPageParam: @escaping GetNextPageParamFunction<TData, TPageParam>,
330+
getPreviousPageParam: GetPreviousPageParamFunction<TData, TPageParam>? = nil,
331+
initialPageParam: TPageParam? = nil,
332+
maxPages: Int? = nil,
333+
retryConfig: RetryConfig = RetryConfig(),
334+
networkMode: NetworkMode = .online,
335+
staleTime: TimeInterval = 0,
336+
gcTime: TimeInterval = defaultGcTime,
337+
refetchTriggers: RefetchTriggers = .default,
338+
refetchOnAppear: RefetchOnAppear = .ifStale,
339+
structuralSharing: Bool = true,
340+
meta: QueryMeta? = nil,
341+
enabled: Bool = true,
342+
queryClient: QueryClient? = nil,
343+
@ViewBuilder content: @escaping (UseInfiniteQueryResult<TData, TPageParam>) -> Content
344+
) where TKey == KeyTuple2<K1, K2> {
345+
let options = InfiniteQueryOptions<TData, QueryError, KeyTuple2<K1, K2>, TPageParam>(
346+
queryKey: queryKey,
347+
queryFn: queryFn,
348+
getNextPageParam: getNextPageParam,
349+
getPreviousPageParam: getPreviousPageParam,
350+
initialPageParam: initialPageParam,
351+
maxPages: maxPages,
352+
retryConfig: retryConfig,
353+
networkMode: networkMode,
354+
staleTime: staleTime,
355+
gcTime: gcTime,
356+
refetchTriggers: refetchTriggers,
357+
refetchOnAppear: refetchOnAppear,
358+
structuralSharing: structuralSharing,
359+
meta: meta,
360+
enabled: enabled
361+
)
362+
363+
self.init(
364+
options: options,
365+
queryClient: queryClient,
366+
content: content
367+
)
368+
}
369+
370+
/// Create UseInfiniteQuery with KeyTuple3-based query key
371+
/// - Parameters:
372+
/// - queryKey: KeyTuple3 identifier for the query
373+
/// - queryFn: Function that fetches page data
374+
/// - getNextPageParam: Function to get next page parameter from pages
375+
/// - getPreviousPageParam: Function to determine the previous page parameter
376+
/// - initialPageParam: Initial page parameter for the first page
377+
/// - maxPages: Maximum number of pages to retain
378+
/// - retryConfig: Configuration for retry behavior (default: RetryConfig())
379+
/// - networkMode: Network behavior configuration (default: .online)
380+
/// - staleTime: Time before data is considered stale (default: 0)
381+
/// - gcTime: Time before unused data is garbage collected (default: 5 minutes)
382+
/// - refetchTriggers: Configuration for automatic refetching triggers (default: .default)
383+
/// - refetchOnAppear: When to refetch data on view appear (default: .ifStale)
384+
/// - structuralSharing: Whether to use structural sharing for performance (default: true)
385+
/// - meta: Arbitrary metadata for this query
386+
/// - enabled: Whether the query should execute automatically (default: true)
387+
/// - queryClient: Optional query client (uses shared instance if nil)
388+
/// - content: View builder that receives the query result
389+
public init<K1: QueryKeyCodable, K2: QueryKeyCodable, K3: QueryKeyCodable>(
390+
queryKey: KeyTuple3<K1, K2, K3>,
391+
queryFn: @escaping @Sendable (KeyTuple3<K1, K2, K3>, TPageParam?) async throws -> TData,
392+
getNextPageParam: @escaping GetNextPageParamFunction<TData, TPageParam>,
393+
getPreviousPageParam: GetPreviousPageParamFunction<TData, TPageParam>? = nil,
394+
initialPageParam: TPageParam? = nil,
395+
maxPages: Int? = nil,
396+
retryConfig: RetryConfig = RetryConfig(),
397+
networkMode: NetworkMode = .online,
398+
staleTime: TimeInterval = 0,
399+
gcTime: TimeInterval = defaultGcTime,
400+
refetchTriggers: RefetchTriggers = .default,
401+
refetchOnAppear: RefetchOnAppear = .ifStale,
402+
structuralSharing: Bool = true,
403+
meta: QueryMeta? = nil,
404+
enabled: Bool = true,
405+
queryClient: QueryClient? = nil,
406+
@ViewBuilder content: @escaping (UseInfiniteQueryResult<TData, TPageParam>) -> Content
407+
) where TKey == KeyTuple3<K1, K2, K3> {
408+
let options = InfiniteQueryOptions<TData, QueryError, KeyTuple3<K1, K2, K3>, TPageParam>(
409+
queryKey: queryKey,
410+
queryFn: queryFn,
411+
getNextPageParam: getNextPageParam,
412+
getPreviousPageParam: getPreviousPageParam,
413+
initialPageParam: initialPageParam,
414+
maxPages: maxPages,
415+
retryConfig: retryConfig,
416+
networkMode: networkMode,
417+
staleTime: staleTime,
418+
gcTime: gcTime,
419+
refetchTriggers: refetchTriggers,
420+
refetchOnAppear: refetchOnAppear,
421+
structuralSharing: structuralSharing,
422+
meta: meta,
423+
enabled: enabled
424+
)
425+
426+
self.init(
427+
options: options,
428+
queryClient: queryClient,
429+
content: content
430+
)
431+
}
432+
433+
/// Create UseInfiniteQuery with KeyTuple4-based query key
434+
/// - Parameters:
435+
/// - queryKey: KeyTuple4 identifier for the query
436+
/// - queryFn: Function that fetches page data
437+
/// - getNextPageParam: Function to get next page parameter from pages
438+
/// - getPreviousPageParam: Function to determine the previous page parameter
439+
/// - initialPageParam: Initial page parameter for the first page
440+
/// - maxPages: Maximum number of pages to retain
441+
/// - retryConfig: Configuration for retry behavior (default: RetryConfig())
442+
/// - networkMode: Network behavior configuration (default: .online)
443+
/// - staleTime: Time before data is considered stale (default: 0)
444+
/// - gcTime: Time before unused data is garbage collected (default: 5 minutes)
445+
/// - refetchTriggers: Configuration for automatic refetching triggers (default: .default)
446+
/// - refetchOnAppear: When to refetch data on view appear (default: .ifStale)
447+
/// - structuralSharing: Whether to use structural sharing for performance (default: true)
448+
/// - meta: Arbitrary metadata for this query
449+
/// - enabled: Whether the query should execute automatically (default: true)
450+
/// - queryClient: Optional query client (uses shared instance if nil)
451+
/// - content: View builder that receives the query result
452+
public init<K1: QueryKeyCodable, K2: QueryKeyCodable, K3: QueryKeyCodable, K4: QueryKeyCodable>(
453+
queryKey: KeyTuple4<K1, K2, K3, K4>,
454+
queryFn: @escaping @Sendable (KeyTuple4<K1, K2, K3, K4>, TPageParam?) async throws -> TData,
329455
getNextPageParam: @escaping GetNextPageParamFunction<TData, TPageParam>,
330456
getPreviousPageParam: GetPreviousPageParamFunction<TData, TPageParam>? = nil,
331457
initialPageParam: TPageParam? = nil,
@@ -341,8 +467,8 @@ extension UseInfiniteQuery {
341467
enabled: Bool = true,
342468
queryClient: QueryClient? = nil,
343469
@ViewBuilder content: @escaping (UseInfiniteQueryResult<TData, TPageParam>) -> Content
344-
) where TKey == String {
345-
let options = InfiniteQueryOptions<TData, QueryError, String, TPageParam>(
470+
) where TKey == KeyTuple4<K1, K2, K3, K4> {
471+
let options = InfiniteQueryOptions<TData, QueryError, KeyTuple4<K1, K2, K3, K4>, TPageParam>(
346472
queryKey: queryKey,
347473
queryFn: queryFn,
348474
getNextPageParam: getNextPageParam,

0 commit comments

Comments
 (0)