Skip to content

Commit 0bc18ce

Browse files
committed
daily cache
1 parent efe72d4 commit 0bc18ce

7 files changed

Lines changed: 83 additions & 34 deletions

File tree

Backend/Sources/Core/KeyFactory.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public struct KeyFactory: Sendable {
3636

3737
public func pageImageKey(
3838
prefix: String,
39+
date: Date = .now,
3940
context: PageContext,
4041
countryName: String? = nil
4142
) -> String {
@@ -44,7 +45,7 @@ public struct KeyFactory: Sendable {
4445
let countryComponent = countryKeySuffix(countryName: countryName) ?? "anywhere"
4546

4647
return
47-
"\(trimmedPrefix(prefix))/page-cache/\(context.pageType.rawValue)/\(normalizedPagePath)-\(countryComponent).png"
48+
"\(trimmedPrefix(prefix))/page-cache/\(date.formatted(dateStyle))/\(context.pageType.rawValue)/\(normalizedPagePath)-\(countryComponent).png"
4849
}
4950

5051
public func generatedImagePrefix(prefix: String, for date: Date) -> String {

Backend/Sources/Server/ImageGenService.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ struct ImageGenService: Sendable {
3535
let countrySuffix = keyFactory.countryKeySuffix(countryName: countryName)
3636
let pageImageKey = keyFactory.pageImageKey(
3737
prefix: environment.generatedImagesPrefix,
38+
date: currentDate,
3839
context: request.context,
3940
countryName: countryName
4041
)
Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import Foundation
12
import Testing
23

34
@testable import Core
45

56
struct KeyFactoryTests {
6-
@Test func pageImageKeyBuildsStableReadableCachePath() {
7+
@Test func pageImageKeyBuildsDailyReadableCachePath() {
78
let keyFactory = KeyFactory()
89

910
let key = keyFactory.pageImageKey(
1011
prefix: "/generated/v2/",
12+
date: Date(timeIntervalSince1970: 0),
1113
context: PageContext(
1214
pagePath: "/posts/Cafe-con-leche/",
1315
pageType: .article
@@ -16,20 +18,46 @@ struct KeyFactoryTests {
1618
)
1719

1820
#expect(
19-
key == "generated/v2/page-cache/article/posts/cafe-con-leche-cote-d-ivoire.png")
21+
key
22+
== "generated/v2/page-cache/1970/01/01/article/posts/cafe-con-leche-cote-d-ivoire.png"
23+
)
2024
}
2125

2226
@Test func pageImageKeyFallsBackToRootAndAnywhere() {
2327
let keyFactory = KeyFactory()
2428

2529
let key = keyFactory.pageImageKey(
2630
prefix: "generated/v2",
31+
date: Date(timeIntervalSince1970: 0),
2732
context: PageContext(
2833
pagePath: "/",
2934
pageType: .index
3035
)
3136
)
3237

33-
#expect(key == "generated/v2/page-cache/index/root-anywhere.png")
38+
#expect(key == "generated/v2/page-cache/1970/01/01/index/root-anywhere.png")
39+
}
40+
41+
@Test func pageImageKeyChangesWithUTCDate() {
42+
let keyFactory = KeyFactory()
43+
let context = PageContext(
44+
pagePath: "/posts/Cafe-con-leche/",
45+
pageType: .article
46+
)
47+
48+
let firstDayKey = keyFactory.pageImageKey(
49+
prefix: "generated/v2",
50+
date: Date(timeIntervalSince1970: 0),
51+
context: context
52+
)
53+
let nextDayKey = keyFactory.pageImageKey(
54+
prefix: "generated/v2",
55+
date: Date(timeIntervalSince1970: 86_400),
56+
context: context
57+
)
58+
59+
#expect(firstDayKey != nextDayKey)
60+
#expect(firstDayKey.contains("/1970/01/01/"))
61+
#expect(nextDayKey.contains("/1970/01/02/"))
3462
}
3563
}

BytesizedCafe/Sources/BytesizedCafe/BytesizedCafe.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct BytesizedCafe {
99
}
1010

1111
enum StorageKeys: String {
12+
case cacheDate = "bytesized-cafe-cache-date"
1213
case imageURL = "bytesized-cafe-image-url"
1314
case pagePath = "bytesized-cafe-page-path"
1415
case pageType = "bytesized-cafe-page-type"
@@ -71,6 +72,8 @@ struct BytesizedCafe {
7172
private static func cachedImageURL(for configuration: Config) -> URL? {
7273
guard
7374
let sessionStorage,
75+
sessionStorage.getItem?(StorageKeys.cacheDate.rawValue).string
76+
== currentUTCDayKey(),
7477
sessionStorage.getItem?(StorageKeys.pagePath.rawValue).string
7578
== configuration.pageContext.pagePath,
7679
sessionStorage.getItem?(StorageKeys.pageType.rawValue).string
@@ -88,6 +91,7 @@ struct BytesizedCafe {
8891
return
8992
}
9093

94+
_ = sessionStorage.setItem?(StorageKeys.cacheDate.rawValue, currentUTCDayKey())
9195
_ = sessionStorage.setItem?(
9296
StorageKeys.pagePath.rawValue, configuration.pageContext.pagePath)
9397
_ = sessionStorage.setItem?(
@@ -107,6 +111,10 @@ struct BytesizedCafe {
107111
"👨🏻‍🍳🍲😋")
108112
}
109113

114+
private static func currentUTCDayKey() -> String {
115+
String(JSDate().toISOString().prefix(10))
116+
}
117+
110118
private static func updateState(_ state: PreparationState, root: JSObject) {
111119
root.dataset.object?[ObjectKeys.started.rawValue] = JSValue.string(state.rawValue)
112120
root.dataset.object?[ObjectKeys.state.rawValue] = JSValue.string(state.rawValue)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ AWS_ACCESS_KEY_ID=<key-id> \
7777
AWS_SECRET_ACCESS_KEY=<secret> \
7878
swift run Server
7979
```
80-
Image generation is capped at 15 images per UTC day. The backend stores a stable per-page image key so repeat requests for the same page reuse the existing image instead of generating again. When the daily budget is exhausted for a first-time page request, the server assigns that page a random previously generated image.
81-
Freshly generated image keys are still partitioned by UTC date under `IMAGE_GEN_PREFIX/YYYY/MM/DD/`, and the stable page cache lives under `IMAGE_GEN_PREFIX/page-cache/`.
80+
Image generation is capped at 15 images per UTC day. The backend stores a daily per-page image key so repeat requests for the same page on the same UTC day reuse the existing image instead of generating again. When the daily budget is exhausted for a first-time page request, the server assigns that page a random previously generated image.
81+
Freshly generated image keys are partitioned by UTC date under `IMAGE_GEN_PREFIX/YYYY/MM/DD/`, and daily page cache keys live under `IMAGE_GEN_PREFIX/page-cache/YYYY/MM/DD/`.
8282

8383
Point the site generator at the backend API when building the HTML:
8484

SPEC.md

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
Implement a web app where:
55
- The page loads in the browser as a SwiftWASM app.
66
- The app automatically requests an image on the home page, article pages, and paginated archive pages.
7-
- A same-session revisit of the same page reuses that page's last returned image from client-side session storage when available.
8-
- The backend persists a stable per-page image key so repeat requests for the same page and request country reuse the existing image instead of generating a new one.
7+
- A same-session revisit of the same page on the same UTC day reuses that page's last returned image from client-side session storage when available.
8+
- The backend persists a daily per-page image key so repeat requests for the same page, UTC day, and request country reuse the existing image instead of generating a new one.
9+
- Returning on the next UTC day uses a new page-cache key, causing the backend to generate or assign a new image for that page.
910
- When the daily generation budget is exhausted, the backend returns a random previously generated image instead of requesting a new one.
1011
- The backend waits for image generation to finish before replying.
1112
- The final image is rendered from a public S3 HTTPS URL.
@@ -24,16 +25,16 @@ Implement a web app where:
2425
### 2.2 High-Level Flow
2526
1. The browser loads the SwiftWASM bundle and page HTML.
2627
2. The app reads the page context from the mount element.
27-
3. If session storage already contains an image URL for the same page path and page type, the app reuses that URL and skips the API call.
28+
3. If session storage already contains an image URL for the same page path, page type, and UTC day, the app reuses that URL and skips the API call.
2829
4. Otherwise, the app calls `POST <API_URL>` with page context.
2930
5. The server derives the client IP from proxy forwarding headers, preferring `X-Real-IP` when present and otherwise falling back to `X-Forwarded-For`, then looks up the origin country with `country.is`.
30-
6. The server validates input and checks for a stable page-cache key derived from page context and resolved country before considering a new generation.
31+
6. The server validates input and checks for a daily page-cache key derived from the current UTC date, page context, and resolved country before considering a new generation.
3132
7. If the page-cache key already exists, the server returns that image immediately.
3233
8. Otherwise, the server counts generated PNG objects already present under the current UTC day prefix in S3 to decide whether its soft daily generation budget has remaining capacity.
33-
9. If budget remains, the server creates a fresh unique dated image key, builds a country-aware prompt when country lookup succeeded, calls OpenAI, uploads the PNG to S3, writes the same image to the stable page-cache key, and returns `200 OK`.
34-
10. If the daily budget is exhausted, the server selects a random existing generated PNG from S3, copies it to the stable page-cache key, and returns `200 OK` with that page-cache image instead.
35-
9. The app swaps the placeholder image source to the returned or cached URL.
36-
10. On successful API responses, the app stores the returned image URL in session storage for future visits to the same page in the current browser session.
34+
9. If budget remains, the server creates a fresh unique dated image key, builds a country-aware prompt when country lookup succeeded, calls OpenAI, uploads the PNG to S3, writes the same image to the daily page-cache key, and returns `200 OK`.
35+
10. If the daily budget is exhausted, the server selects a random existing generated PNG from S3, copies it to the daily page-cache key, and returns `200 OK` with that page-cache image instead.
36+
11. The app swaps the placeholder image source to the returned or cached URL.
37+
12. On successful API responses, the app stores the returned image URL in session storage with the current UTC date for future visits to the same page on the same UTC day in the current browser session.
3738

3839
### 2.3 Published SwiftWASM Assets
3940
- The `BytesizedCafe` SwiftWASM package is built into the repo-root `bytesized-cafe-app/` directory.
@@ -58,17 +59,17 @@ The backend derives the public image origin from `GENERATED_IMAGES_BUCKET` and `
5859
- Freshly generated image:
5960
- `{IMAGE_GEN_PREFIX}/{YYYY}/{MM}/{DD}/{UUID}-{country-slug}.png` when the request country is known
6061
- `{IMAGE_GEN_PREFIX}/{YYYY}/{MM}/{DD}/{UUID}.png` when the request country is not known
61-
- Stable page-cache image:
62-
- `{IMAGE_GEN_PREFIX}/page-cache/{pageType}/{normalized-page-path}-{country-slug}.png` when the request country is known
63-
- `{IMAGE_GEN_PREFIX}/page-cache/{pageType}/{normalized-page-path}-anywhere.png` when the request country is not known
62+
- Daily page-cache image:
63+
- `{IMAGE_GEN_PREFIX}/page-cache/{YYYY}/{MM}/{DD}/{pageType}/{normalized-page-path}-{country-slug}.png` when the request country is known
64+
- `{IMAGE_GEN_PREFIX}/page-cache/{YYYY}/{MM}/{DD}/{pageType}/{normalized-page-path}-anywhere.png` when the request country is not known
6465
- Random fallback image:
65-
- Prefer an existing PNG under `IMAGE_GEN_PREFIX/` whose key ends in the current request's `-{country-slug}.png`
66-
- Fall back to any existing PNG under `IMAGE_GEN_PREFIX/` when no country-matching image is available
66+
- Prefer an existing PNG under the current UTC date prefix whose key ends in the current request's `-{country-slug}.png`
67+
- Fall back to any existing PNG under the current UTC date prefix when no country-matching image is available
6768

6869
Rules:
6970
- Fresh generation keys must not be derived from page context.
70-
- Stable page-cache keys must be derived from page context and resolved country.
71-
- API responses should prefer the stable page-cache key whenever one exists or is created during the request.
71+
- Daily page-cache keys must be derived from the current UTC date, page context, and resolved country.
72+
- API responses should prefer the daily page-cache key whenever one exists or is created during the request.
7273

7374
### 3.4 Object Metadata
7475
When uploading a freshly generated image:
@@ -117,13 +118,13 @@ Response:
117118

118119
```json
119120
{
120-
"url": "https://<public-base-domain>/generated/v2/page-cache/article/posts/example-article-france.png"
121+
"url": "https://<public-base-domain>/generated/v2/page-cache/2026/04/23/article/posts/example-article-france.png"
121122
}
122123
```
123124

124125
Rules:
125126
- `url` is the final public image URL and must use the generated-images bucket public origin.
126-
- The response may return a stable per-page cache key when the page already has an assigned image.
127+
- The response may return a daily per-page cache key when the page already has an assigned image for the current UTC day.
127128
- Return `200` only after the image has been uploaded successfully or a random fallback image has been selected successfully.
128129
- Invalid input returns `4xx`.
129130
- If the daily budget is exhausted and no fallback image exists, return `503`.
@@ -136,7 +137,7 @@ Rules:
136137
- Encapsulate S3 operations behind one `S3ImageStore` client object that owns the bucket configuration and AWS client lifecycle for image upload and lookup operations.
137138
- Resolve the client IP address by preferring `X-Real-IP` when present and otherwise falling back to `X-Forwarded-For`.
138139
- Look up the request origin country with `https://api.country.is/{ip}` and convert the returned region code into an English country name when available.
139-
- Derive a stable page-cache key from page context and resolved country, and return it immediately when that object already exists in S3.
140+
- Derive a daily page-cache key from the current UTC date, page context, and resolved country, and return it immediately when that object already exists in S3.
140141
- Check the soft daily generation budget by counting PNG objects already present under the current UTC date prefix in S3.
141142
- Build the public `url`.
142143
- When budget remains:
@@ -147,21 +148,21 @@ Rules:
147148
- Fall back to the same prompt structure scoped to somewhere in the world when the client IP or country cannot be resolved.
148149
- Call the OpenAI image generation API with model `gpt-image-1.5`.
149150
- Upload the PNG to the generated image key used for the dated generation pool.
150-
- Upload the same PNG to the stable page-cache key.
151+
- Upload the same PNG to the daily page-cache key.
151152
- Return the page-cache `url`.
152153
- When budget is exhausted:
153-
- Prefer a random existing generated PNG key from S3 whose key suffix matches the current request country.
154-
- Fall back to a random existing generated PNG key from S3 when no country-matching key is available.
155-
- Copy the selected fallback image to the stable page-cache key without calling OpenAI.
154+
- Prefer a random existing generated PNG key from the current UTC date prefix whose key suffix matches the current request country.
155+
- Fall back to a random existing generated PNG key from the current UTC date prefix when no country-matching key is available.
156+
- Copy the selected fallback image to the daily page-cache key without calling OpenAI.
156157
- Return the page-cache `url`.
157158

158159
## 7. Frontend Behavior
159160
- Show a loading placeholder immediately.
160161
- Read page context from the mount element.
161-
- If session storage contains a URL for the same page path and page type, reuse that URL and skip the API call.
162+
- If session storage contains a URL for the same page path, page type, and UTC day, reuse that URL and skip the API call.
162163
- Otherwise, start a single `POST` request to the configured API URL.
163164
- When the request succeeds, swap the placeholder image source to the returned `url`.
164-
- Persist the returned image URL in session storage keyed to the current page so the next same-session visit of that page can reuse it.
165+
- Persist the returned image URL in session storage keyed to the current page and UTC day so the next same-session visit of that page can reuse it only until the UTC day changes.
165166

166167
## 8. Environment Variables
167168

@@ -187,14 +188,15 @@ Local repo tooling may provide `BACKEND_HOST` and `BACKEND_PORT` as aliases for
187188

188189
## 9. Validation
189190
The implementation is considered complete when:
190-
- A same-session revisit of the same page reuses the last returned image URL from session storage without making a new backend request.
191-
- A backend request for a page that already has a stable page-cache object returns that existing image URL without making a new OpenAI request.
191+
- A same-session revisit of the same page on the same UTC day reuses the last returned image URL from session storage without making a new backend request.
192+
- A same-session revisit of the same page after the UTC day changes makes a backend request instead of reusing yesterday's session-storage URL.
193+
- A backend request for a page that already has a daily page-cache object returns that existing image URL without making a new OpenAI request.
192194
- The backend returns `200` only after a fresh image upload succeeds or a random fallback image has been selected.
193195
- When the daily budget is exhausted, the backend returns a random existing generated image instead of making a new OpenAI request.
194196
- Fresh generations use the request origin country in the prompt when the server can resolve it from the client IP, and otherwise fall back to the generic worldwide prompt.
195197
- Fresh generations include a country slug suffix in the image key when the request country is known.
196198
- When the daily budget is exhausted, fallback selection prefers existing images whose keys match the current request country and otherwise falls back to any existing image.
197-
- The backend persists deterministic per-page cache keys separately from the dated generation pool.
199+
- The backend persists deterministic daily per-page cache keys separately from the dated generation pool.
198200

199201
## 10. Deployment
200202

@@ -223,3 +225,4 @@ The implementation is considered complete when:
223225
- The repo's Swift package manifests target Swift tools version `6.3`, the macOS GitHub Actions job installs Swift `6.3.0`, and the SwiftWasm site build uses the compatible `swift-6.3-RELEASE` SDK tag.
224226
- `Scripts/run-local.sh` provides a one-command local stack for development and opens the local site in the default browser after the backend and static site server are ready.
225227
- The script rebuilds the `BytesizedCafe` SwiftWASM bundle, regenerates the site with `BYTESIZED_CAFE_API_URL` pointed at a localhost backend, prebuilds the backend to avoid counting SwiftPM compilation against the startup timeout, starts the Hummingbird server, and serves `Output/` over a local static HTTP server.
228+
- `Scripts/build-bytesized-cafe-app.sh` prefers a SwiftWASM SDK ID matching the active `swift --version` release when multiple WASM SDKs are installed; `SWIFT_WASM_SDK_ID` or `SWIFT_SDK_ID` can still override the auto-detected SDK.

Scripts/build-bytesized-cafe-app.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ PACKAGE_OUTPUT_DIR="${BYTESIZED_CAFE_DIR}/.build/plugins/PackageToJS/outputs/Pac
99
PRODUCT_NAME="BytesizedCafe"
1010
SDK_LIST="$(swift sdk list)"
1111
SWIFT_WASM_SDK_ID="${SWIFT_WASM_SDK_ID:-${SWIFT_SDK_ID:-}}"
12+
SWIFT_VERSION="$(swift --version | sed -n '1s/.*Swift version \([0-9][0-9.]*\).*/\1/p')"
13+
PREFERRED_SWIFT_WASM_SDK_ID=""
14+
15+
if [[ -n "${SWIFT_VERSION}" ]]; then
16+
PREFERRED_SWIFT_WASM_SDK_ID="swift-${SWIFT_VERSION}-RELEASE_wasm"
17+
fi
1218

1319
if [[ -z "${SWIFT_WASM_SDK_ID}" ]]; then
14-
if grep -Fxq "wasm32-unknown-wasi" <<< "${SDK_LIST}"; then
20+
if [[ -n "${PREFERRED_SWIFT_WASM_SDK_ID}" ]] && grep -Fxq "${PREFERRED_SWIFT_WASM_SDK_ID}" <<< "${SDK_LIST}"; then
21+
SWIFT_WASM_SDK_ID="${PREFERRED_SWIFT_WASM_SDK_ID}"
22+
elif grep -Fxq "wasm32-unknown-wasi" <<< "${SDK_LIST}"; then
1523
SWIFT_WASM_SDK_ID="wasm32-unknown-wasi"
1624
else
1725
SWIFT_WASM_SDK_ID="$(grep 'wasm' <<< "${SDK_LIST}" | grep -v 'embedded' | head -n 1 || true)"

0 commit comments

Comments
 (0)