Skip to content

Commit 0362e99

Browse files
authored
fix(core): invalidate RSS caches after content changes (#2802)
* fix(core): invalidate RSS aggregate caches * fix(core): invalidate caches after post deletion
1 parent 63ac7c5 commit 0362e99

4 files changed

Lines changed: 96 additions & 9 deletions

File tree

apps/core/src/modules/aggregate/aggregate.service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,9 @@ export class AggregateService {
539539

540540
@OnEvent(EventBusEvents.CleanAggregateCache)
541541
async cleanCache() {
542-
await this.redisService.getClient().del(CacheKeys.Aggregate)
542+
await Promise.all([
543+
this.redisService.getClient().del(CacheKeys.RSS, CacheKeys.RSSXml),
544+
this.redisService.deleteKeysByPattern(`${CacheKeys.Aggregate}*`),
545+
])
543546
}
544547
}

apps/core/src/modules/post/post.service.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -544,14 +544,19 @@ export class PostService implements OnApplicationBootstrap {
544544
FileReferenceType.Post,
545545
),
546546
])
547-
await this.eventManager.emit(
548-
BusinessEvents.POST_DELETE,
549-
{ id },
550-
{
551-
scope: EventScope.TO_SYSTEM_VISITOR,
552-
nextTick: true,
553-
},
554-
)
547+
await Promise.all([
548+
this.eventManager.emit(EventBusEvents.CleanAggregateCache, null, {
549+
scope: EventScope.TO_SYSTEM,
550+
}),
551+
this.eventManager.emit(
552+
BusinessEvents.POST_DELETE,
553+
{ id },
554+
{
555+
scope: EventScope.TO_SYSTEM_VISITOR,
556+
nextTick: true,
557+
},
558+
),
559+
])
555560
}
556561

557562
async getCategoryBySlug(slug: string) {

apps/core/test/src/modules/aggregate/aggregate.service.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it, vi } from 'vitest'
22

3+
import { API_CACHE_PREFIX, CacheKeys } from '~/constants/cache.constant'
34
import { AggregateService } from '~/modules/aggregate/aggregate.service'
45

56
const noteRow = {
@@ -254,3 +255,54 @@ describe('AggregateService.buildRssStructure', () => {
254255
expect(JSON.parse(data[0].content!).root.children).toHaveLength(2)
255256
})
256257
})
258+
259+
describe('AggregateService.cleanCache', () => {
260+
it('invalidates RSS and aggregate-derived caches without flushing unrelated API caches', async () => {
261+
const del = vi.fn(async () => 2)
262+
const deleteKeysByPattern = vi.fn(async () => 5)
263+
const redisService = {
264+
getClient: vi.fn(() => ({ del })),
265+
deleteKeysByPattern,
266+
}
267+
268+
const service = new AggregateService(
269+
{} as any,
270+
{} as any,
271+
{} as any,
272+
{} as any,
273+
{} as any,
274+
{} as any,
275+
{} as any,
276+
{} as any,
277+
{} as any,
278+
{} as any,
279+
redisService as any,
280+
{} as any,
281+
{} as any,
282+
{} as any,
283+
)
284+
285+
await service.cleanCache()
286+
287+
expect(del).toHaveBeenCalledOnce()
288+
expect(del).toHaveBeenCalledWith(CacheKeys.RSS, CacheKeys.RSSXml)
289+
expect(deleteKeysByPattern).toHaveBeenCalledOnce()
290+
expect(deleteKeysByPattern).toHaveBeenCalledWith(`${CacheKeys.Aggregate}*`)
291+
expect(`${CacheKeys.Aggregate}*`).not.toBe(`${API_CACHE_PREFIX}*`)
292+
})
293+
294+
it('keeps every aggregate-derived cache under the invalidated prefix', () => {
295+
const aggregateKeys = [
296+
CacheKeys.Aggregate,
297+
CacheKeys.AggregateSite,
298+
CacheKeys.SiteMap,
299+
CacheKeys.SiteMapXml,
300+
]
301+
302+
for (const key of aggregateKeys) {
303+
expect(key.startsWith(CacheKeys.Aggregate)).toBe(true)
304+
}
305+
expect(CacheKeys.RSS.startsWith(CacheKeys.Aggregate)).toBe(false)
306+
expect(CacheKeys.RSSXml.startsWith(CacheKeys.Aggregate)).toBe(false)
307+
})
308+
})

apps/core/test/src/modules/post/post.service.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { describe, expect, it, vi } from 'vitest'
44
import { createPgRepositoryMock, now } from '@/helper/pg-repository-mock'
55
import { AppException } from '~/common/errors/exception.types'
66
import { ArticleTypeEnum } from '~/constants/article.constant'
7+
import { BusinessEvents, EventScope } from '~/constants/business-event.constant'
8+
import { EventBusEvents } from '~/constants/event-bus.constant'
79
import {
810
CATEGORY_SERVICE_TOKEN,
911
DRAFT_SERVICE_TOKEN,
@@ -103,6 +105,7 @@ const createService = () => {
103105
commentService,
104106
contentMigrationCommitService,
105107
draftService,
108+
eventManager,
106109
fileReferenceService,
107110
repository,
108111
service,
@@ -268,6 +271,7 @@ describe('PostService', () => {
268271
const {
269272
commentService,
270273
draftService,
274+
eventManager,
271275
fileReferenceService,
272276
repository,
273277
service,
@@ -285,6 +289,29 @@ describe('PostService', () => {
285289
expect(
286290
fileReferenceService.removeReferencesForDocument,
287291
).toHaveBeenCalledWith('post-1', FileReferenceType.Post)
292+
expect(eventManager.emit).toHaveBeenCalledWith(
293+
EventBusEvents.CleanAggregateCache,
294+
null,
295+
{ scope: EventScope.TO_SYSTEM },
296+
)
297+
expect(eventManager.emit).toHaveBeenCalledWith(
298+
BusinessEvents.POST_DELETE,
299+
{ id: 'post-1' },
300+
{
301+
scope: EventScope.TO_SYSTEM_VISITOR,
302+
nextTick: true,
303+
},
304+
)
305+
})
306+
307+
it('does not invalidate aggregate caches when post deletion fails', async () => {
308+
const { eventManager, repository, service } = createService()
309+
repository.findById.mockResolvedValue(createPost())
310+
repository.deleteById.mockRejectedValue(new Error('delete failed'))
311+
312+
await expect(service.deletePost('post-1')).rejects.toThrow('delete failed')
313+
314+
expect(eventManager.emit).not.toHaveBeenCalled()
288315
})
289316

290317
it('rejects missing related post ids', async () => {

0 commit comments

Comments
 (0)