-
Notifications
You must be signed in to change notification settings - Fork 731
Expand file tree
/
Copy pathqueryCache.ts
More file actions
148 lines (130 loc) · 4.36 KB
/
Copy pathqueryCache.ts
File metadata and controls
148 lines (130 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { createHash } from 'crypto'
import { getServiceLogger } from '@crowd/logging'
import { RedisCache, RedisClient } from '@crowd/redis'
import { PageData } from '@crowd/types'
import { IDbMemberData } from './types'
interface IncludeOptions {
identities?: boolean
segments?: boolean
memberOrganizations?: boolean
onlySubProjects?: boolean
maintainers?: boolean
}
const log = getServiceLogger()
export class MemberQueryCache {
private cache: RedisCache
private countCache: RedisCache
private lockCache: RedisCache
constructor(redis: RedisClient) {
this.cache = new RedisCache('members-advanced', redis, log)
this.countCache = new RedisCache('members-count', redis, log)
this.lockCache = new RedisCache('members-refresh-lock', redis, log)
}
buildCacheKey(params: {
fields?: string[]
filter?: Record<string, unknown>
include?: IncludeOptions
includeAllAttributes?: boolean
limit?: number
offset?: number
orderBy?: string
search?: string
segmentId?: string
}): string {
const cleanParams = Object.fromEntries(
Object.entries({
fields: params.fields?.sort(),
filter: params.filter,
include: params.include,
includeAllAttributes: params.includeAllAttributes,
limit: params.limit,
offset: params.offset,
orderBy: params.orderBy,
search: params.search,
segmentId: params.segmentId,
}).filter(([, value]) => value !== null && value !== undefined),
)
const hash = createHash('md5').update(JSON.stringify(cleanParams)).digest('hex')
const filterId = (params.filter?.id as Record<string, unknown>)?.eq
if (filterId && typeof filterId === 'string') {
return `members_advanced:${filterId}:${hash}`
}
return `members_advanced:${hash}`
}
buildCountCacheKey(params: {
filter?: Record<string, unknown>
search?: string
segmentId?: string
}): string {
return this.buildCacheKey({
filter: params.filter,
search: params.search,
segmentId: params.segmentId,
})
}
async get(cacheKey: string): Promise<PageData<IDbMemberData> | null> {
try {
const cachedResult = await this.cache.get(cacheKey)
if (cachedResult) {
return JSON.parse(cachedResult)
}
return null
} catch (error) {
log.warn('Error retrieving from cache', { error })
return null
}
}
async set(cacheKey: string, result: PageData<IDbMemberData>, ttlSeconds: number): Promise<void> {
try {
await this.cache.set(cacheKey, JSON.stringify(result), ttlSeconds)
} catch (error) {
log.warn('Error saving to cache', { error })
}
}
async getCount(cacheKey: string): Promise<number | null> {
try {
const cachedCount = await this.countCache.get(cacheKey)
if (!cachedCount) return null
const parsed = parseInt(cachedCount, 10)
return isNaN(parsed) ? null : parsed
} catch (error) {
log.warn('Error retrieving count from cache', { error })
return null
}
}
async setCount(cacheKey: string, count: number, ttlSeconds: number): Promise<void> {
try {
await this.countCache.set(cacheKey, count.toString(), ttlSeconds)
} catch (error) {
log.warn('Error saving count to cache', { error })
}
}
async invalidateAll(): Promise<void> {
try {
const [resultsDeleted, countsDeleted, locksDeleted] = await Promise.all([
this.cache.deleteAll(),
this.countCache.deleteAll(),
this.lockCache.deleteAll(),
])
log.info(
`Invalidated member query cache: ${resultsDeleted} result entries, ${countsDeleted} count entries, ${locksDeleted} locks`,
)
} catch (error) {
log.warn('Error invalidating member query cache', { error })
}
}
async invalidateByPattern(pattern: string): Promise<void> {
try {
const [resultsDeleted, countsDeleted, locksDeleted] = await Promise.all([
this.cache.deleteByKeyPattern(pattern),
this.countCache.deleteByKeyPattern(pattern),
this.lockCache.deleteByKeyPattern(pattern),
])
log.info(
`Invalidated member query cache by pattern: ${resultsDeleted} result entries, ${countsDeleted} count entries, ${locksDeleted} locks deleted for pattern ${pattern}`,
)
} catch (error) {
log.warn('Error invalidating member query cache by pattern', { error, pattern })
}
}
}