Skip to content

Commit 105089c

Browse files
authored
RSS/Podcast feeds: Invalidate cache when item count changed (#18)
When new items were added, even if those items don’t have a later modification date than the previous generated site, always re-generate all RSS and podcast feeds.
1 parent 37473e5 commit 105089c

File tree

4 files changed

+95
-17
lines changed

4 files changed

+95
-17
lines changed

Sources/Publish/Internal/PodcastFeedGenerator.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal struct PodcastFeedGenerator<Site: Website> where Site.ItemMetadata: Pod
2121
let items = section.items.sorted(by: { $0.date > $1.date })
2222

2323
if let date = context.lastGenerationDate, let cache = oldCache {
24-
if cache.config == config {
24+
if cache.config == config, cache.itemCount == items.count {
2525
let newlyModifiedItem = items.first { $0.lastModified > date }
2626

2727
guard newlyModifiedItem != nil else {
@@ -33,7 +33,7 @@ internal struct PodcastFeedGenerator<Site: Website> where Site.ItemMetadata: Pod
3333
let feed = try makeFeed(containing: items, section: section)
3434
.render(indentedBy: config.indentation)
3535

36-
let newCache = Cache(config: config, feed: feed)
36+
let newCache = Cache(config: config, feed: feed, itemCount: items.count)
3737
try cacheFile.write(newCache.encoded())
3838
try outputFile.write(feed)
3939
}
@@ -43,6 +43,7 @@ private extension PodcastFeedGenerator {
4343
struct Cache: Codable {
4444
let config: PodcastFeedConfiguration<Site>
4545
let feed: String
46+
let itemCount: Int
4647
}
4748

4849
func makeFeed(containing items: [Item<Site>],

Sources/Publish/Internal/RSSFeedGenerator.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal struct RSSFeedGenerator<Site: Website> {
2727
items.sort { $0.date > $1.date }
2828

2929
if let date = context.lastGenerationDate, let cache = oldCache {
30-
if cache.config == config {
30+
if cache.config == config, cache.itemCount == items.count {
3131
let newlyModifiedItem = items.first { $0.lastModified > date }
3232

3333
guard newlyModifiedItem != nil else {
@@ -38,7 +38,7 @@ internal struct RSSFeedGenerator<Site: Website> {
3838

3939
let feed = makeFeed(containing: items).render(indentedBy: config.indentation)
4040

41-
let newCache = Cache(config: config, feed: feed)
41+
let newCache = Cache(config: config, feed: feed, itemCount: items.count)
4242
try cacheFile.write(newCache.encoded())
4343
try outputFile.write(feed)
4444
}
@@ -48,6 +48,7 @@ private extension RSSFeedGenerator {
4848
struct Cache: Codable {
4949
let config: RSSFeedConfiguration
5050
let feed: String
51+
let itemCount: Int
5152
}
5253

5354
func makeFeed(containing items: [Item<Site>]) -> RSS {

Tests/PublishTests/Tests/PodcastFeedGenerationTests.swift

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ final class PodcastFeedGenerationTests: PublishTestCase {
8282

8383
XCTAssertNotEqual(feedA, feedB)
8484
}
85+
86+
func testNotReusingPreviousFeedIfItemWasAdded() throws {
87+
let folder = try Folder.createTemporary()
88+
89+
let audio = try Audio(
90+
url: require(URL(string: "https://audio.mp3")),
91+
duration: Audio.Duration(),
92+
byteSize: 55
93+
)
94+
95+
let itemA = Item<Site>(
96+
path: "a",
97+
sectionID: .one,
98+
metadata: .init(podcast: .init()),
99+
content: Content(audio: audio)
100+
)
101+
102+
let itemB = Item<Site>(
103+
path: "b",
104+
sectionID: .one,
105+
metadata: .init(podcast: .init()),
106+
content: Content(
107+
lastModified: itemA.lastModified,
108+
audio: audio
109+
)
110+
)
111+
112+
try generateFeed(in: folder, generationSteps: [
113+
.addItem(itemA)
114+
])
115+
116+
let feedA = try folder.file(at: "Output/feed.rss").readAsString()
117+
118+
try generateFeed(in: folder, generationSteps: [
119+
.addItem(itemA),
120+
.addItem(itemB)
121+
])
122+
123+
let feedB = try folder.file(at: "Output/feed.rss").readAsString()
124+
XCTAssertNotEqual(feedA, feedB)
125+
}
85126
}
86127

87128
extension PodcastFeedGenerationTests {
@@ -90,13 +131,15 @@ extension PodcastFeedGenerationTests {
90131
("testOnlyIncludingSpecifiedSection", testOnlyIncludingSpecifiedSection),
91132
("testConvertingRelativeLinksToAbsolute", testConvertingRelativeLinksToAbsolute),
92133
("testReusingPreviousFeedIfNoItemsWereModified", testReusingPreviousFeedIfNoItemsWereModified),
93-
("testNotReusingPreviousFeedIfConfigChanged", testNotReusingPreviousFeedIfConfigChanged)
134+
("testNotReusingPreviousFeedIfConfigChanged", testNotReusingPreviousFeedIfConfigChanged),
135+
("testNotReusingPreviousFeedIfItemWasAdded", testNotReusingPreviousFeedIfItemWasAdded)
94136
]
95137
}
96138
}
97139

98140
private extension PodcastFeedGenerationTests {
99-
typealias Configuration = PodcastFeedConfiguration<WebsiteStub.WithPodcastMetadata>
141+
typealias Site = WebsiteStub.WithPodcastMetadata
142+
typealias Configuration = PodcastFeedConfiguration<Site>
100143

101144
func makeConfigStub() throws -> Configuration {
102145
try Configuration(
@@ -123,12 +166,17 @@ private extension PodcastFeedGenerationTests {
123166
"""
124167
}
125168

126-
func generateFeed(in folder: Folder,
127-
config: Configuration? = nil,
128-
date: Date = Date(),
129-
content: [Path : String] = [:]) throws {
169+
func generateFeed(
170+
in folder: Folder,
171+
config: Configuration? = nil,
172+
generationSteps: [PublishingStep<Site>] = [
173+
.addMarkdownFiles()
174+
],
175+
date: Date = Date(),
176+
content: [Path : String] = [:]
177+
) throws {
130178
try publishWebsiteWithPodcast(in: folder, using: [
131-
.addMarkdownFiles(),
179+
.group(generationSteps),
132180
.generatePodcastFeed(
133181
for: .one,
134182
config: config ?? makeConfigStub(),

Tests/PublishTests/Tests/RSSFeedGenerationTests.swift

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,26 @@ final class RSSFeedGenerationTests: PublishTestCase {
7676

7777
XCTAssertNotEqual(feedA, feedB)
7878
}
79+
80+
func testNotReusingPreviousFeedIfItemWasAdded() throws {
81+
let folder = try Folder.createTemporary()
82+
let itemA = Item.stub()
83+
let itemB = Item.stub().setting(\.lastModified, to: itemA.lastModified)
84+
85+
try generateFeed(in: folder, generationSteps: [
86+
.addItem(itemA)
87+
])
88+
89+
let feedA = try folder.file(at: "Output/feed.rss").readAsString()
90+
91+
try generateFeed(in: folder, generationSteps: [
92+
.addItem(itemA),
93+
.addItem(itemB)
94+
])
95+
96+
let feedB = try folder.file(at: "Output/feed.rss").readAsString()
97+
XCTAssertNotEqual(feedA, feedB)
98+
}
7999
}
80100

81101
extension RSSFeedGenerationTests {
@@ -84,18 +104,26 @@ extension RSSFeedGenerationTests {
84104
("testOnlyIncludingSpecifiedSections", testOnlyIncludingSpecifiedSections),
85105
("testConvertingRelativeLinksToAbsolute", testConvertingRelativeLinksToAbsolute),
86106
("testReusingPreviousFeedIfNoItemsWereModified", testReusingPreviousFeedIfNoItemsWereModified),
87-
("testNotReusingPreviousFeedIfConfigChanged", testNotReusingPreviousFeedIfConfigChanged)
107+
("testNotReusingPreviousFeedIfConfigChanged", testNotReusingPreviousFeedIfConfigChanged),
108+
("testNotReusingPreviousFeedIfItemWasAdded", testNotReusingPreviousFeedIfItemWasAdded)
88109
]
89110
}
90111
}
91112

92113
private extension RSSFeedGenerationTests {
93-
func generateFeed(in folder: Folder,
94-
config: RSSFeedConfiguration = .default,
95-
date: Date = Date(),
96-
content: [Path : String] = [:]) throws {
114+
typealias Site = WebsiteStub.WithoutItemMetadata
115+
116+
func generateFeed(
117+
in folder: Folder,
118+
config: RSSFeedConfiguration = .default,
119+
generationSteps: [PublishingStep<Site>] = [
120+
.addMarkdownFiles()
121+
],
122+
date: Date = Date(),
123+
content: [Path : String] = [:]
124+
) throws {
97125
try publishWebsite(in: folder, using: [
98-
.addMarkdownFiles(),
126+
.group(generationSteps),
99127
.generateRSSFeed(
100128
including: [.one],
101129
config: config,

0 commit comments

Comments
 (0)