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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
//! Caching for Dioxus fullstack apps (the `cache` feature): on the device,
//! on the server, and at the CDN, with checks against caching the wrong
//! thing in the wrong place. Everything here is also re-exported at the crate
//! root.
//!
//! ```ignore
//! use g3_kit::{cache_shared, invalidate_cached, use_cached};
//!
//! // A screen: show the last known answer at once, refetch in the background.
//! let media = use_cached(get_media, (id.clone(),));
//!
//! // After a mutation: refetch what it changed.
//! save_rating(id.clone(), score).await?;
//! invalidate_cached(get_my_rating);
//!
//! // A public read: cached at the CDN and on the server for 5 minutes.
//! #[cache_shared(cdn = 300, server = "5m")]
//! #[get("/api/trending?media_type", db: Db)]
//! pub async fn get_trending(media_type: Option<MediaType>) -> Result<Vec<Media>> { .. }
//! ```
//!
//! Every name says it is about caching, so the crate reads well through a
//! `use` statement.
//!
//! # Choosing a cache
//!
//! A read can be cached in three places. Each answers a different problem,
//! and none replaces another.
//!
//! | | [Client](#client-cache) | [Server](#server-cache) | [CDN](#cdn-cache) |
//! |---|---|---|---|
//! | **For** | Showing the last known data at once, then refreshing it | Not repeating slow or rate-limited work | Answering identical public requests without reaching the server |
//! | **Holds** | One user's data, on one device | Answers shared by every request, in one server process | Whole HTTP responses shared by every visitor |
//! | **Refreshed by** | Screen opens, [`invalidate_cached`], app focus | Expiry only | Expiry only |
//! | **API** | [`use_cached`] | [`cache_shared`]`(server = ..)`, `ServerCache` | [`cache_shared`]`(cdn = ..)`, `cdn_cache_guard` |
//!
//! **The rule:** data that depends on who is asking (their ratings, their
//! lists) is cached on the client only. Data that is the same for everyone
//! (trending titles, a catalog) may also be cached on the server and at the
//! CDN.
//!
//! A read passes through them in order: client memory and store, then the
//! browser's HTTP cache and the CDN, then the server, whose own cache may
//! answer instead of the database or an outside API.
//!
//! # Setup
//!
//! Enable the feature matching each build:
//!
//! ```toml
//! [dependencies]
//! g3-kit = { version = "0.1", features = ["cache"] }
//!
//! [features]
//! web = ["dioxus/web", "g3-kit/web"] # IndexedDB store
//! mobile = ["dioxus/mobile", "g3-kit/mobile"] # redb file store
//! server = ["dioxus/server", "g3-kit/server"] # server + CDN caches; client cache off
//! ```
//!
//! Then, in the app:
//!
//! ```ignore
//! use g3_kit::{CacheConfig, set_cache_owner, use_client_cache};
//!
//! fn App() -> Element {
//! // 1. Once, first thing in the root component.
//! use_client_cache(CacheConfig::new("my-app"));
//!
//! // 2. Whenever the signed-in user is known or changes, and on sign-out.
//! let user = use_server_future(get_current_user)?;
//! use_effect(move || {
//! let owner = user().and_then(|user| user.ok()).flatten().map(|user| user.id);
//! spawn(set_cache_owner(owner));
//! });
//! // ...
//! }
//! ```
//!
//! And on the server router, once, outside the session layer:
//!
//! ```ignore
//! .layer(session_layer)
//! .layer(g3_kit::cdn_cache_guard("/api"))
//! ```
//!
//! # Client cache
//!
//! Use [`use_cached`] in place of `use_resource` for data a screen shows
//! when it opens:
//!
//! ```ignore
//! let media = use_cached(get_media, (id.clone(),));
//! ```
//!
//! The screen shows what the device last saw (from memory, or from disk on
//! a cold start) and refetches in the background; the fresh answer replaces
//! it. After a mutation, [`invalidate_cached`] each read it changed:
//!
//! ```ignore
//! save_rating(id.clone(), score).await?;
//! invalidate_cached(get_my_rating);
//! invalidate_cached(get_my_ratings);
//! ```
//!
//! Don't use it for reads that change with every keystroke (use
//! `use_resource`), or in a handler that must act on current server state
//! (call the server function directly).
//!
//! ## Several devices
//!
//! The server is the only source of truth: mutations go straight to it, and
//! nothing is written offline, so devices never need merging. The question
//! is only how long a device shows what it saw last:
//!
//! - **A screen that opens** shows its old copy and refetches at once.
//! - **A screen that stayed open** refetches when the app comes back into
//! view ([`CacheConfig::revalidate_on_focus`], on by default).
//! - **A tap on stale data** is still sent. Write mutations so that is
//! harmless: `set_in_list(list, item, true)` rather than
//! `toggle_in_list(list, item)`, which undoes the other device's change
//! when this one shows old state. A full reorder should place items it
//! was not sent after the ones it was.
//!
//! # Server cache
//!
//! For work worth skipping: a slow query, an outside API with a rate limit
//! or a price. A read by id from your own database rarely is.
//!
//! When the whole answer is the same for every visitor, cache the whole
//! server function:
//!
//! ```ignore
//! #[cache_shared(server = "5m")]
//! #[get("/api/trending?media_type", db: Db)]
//! pub async fn get_trending(media_type: Option<MediaType>) -> Result<Vec<Media>> { .. }
//! ```
//!
//! Otherwise, cache the expensive part in a `ServerCache` static, with the
//! user's id in the key when the answer depends on the user:
//!
//! ```ignore
//! static DESCRIPTIONS: ServerCache<String, Option<Description>> =
//! ServerCache::new(Duration::from_secs(60 * 60), 10_000);
//! ```
//!
//! Server caches only expire. Anything a user can change and then expects
//! to see changed should not be cached there; see `server`.
//!
//! # CDN cache
//!
//! For public `GET` answers read by many visitors. [`cache_shared`]`(cdn = ..)`
//! marks a function's successful responses `public`; `cdn_cache_guard`
//! keeps everything else private and strips session cookies from what is
//! shared. Only standard `Cache-Control` headers are used, so any CDN works
//! (Cloudflare, CloudFront, Fastly, Vercel, a reverse proxy); most need a
//! rule making API paths eligible. See `cdn` for each provider.
//!
//! # What is checked for you, and what is not
//!
//! | Mistake | Caught |
//! |---|---|
//! | Passing a closure where a server function is expected | Panics on first use, with the fix |
//! | `#[cache_shared]` on a function that binds a session, auth, user or cookie extractor | Compile error |
//! | `#[cache_shared]` on a function whose body reads `FullstackContext` | Compile error |
//! | `#[cache_shared]` on a `POST`, `PUT`, `PATCH`, `DELETE` or `#[server]` | Compile error |
//! | `#[cache_shared]` placed below `#[get]`, or with a malformed duration | Compile error |
//! | `#[cache_shared]` in a server build without `g3-kit/server` | Warning naming the fix; runs uncached |
//! | A shared response carrying a session cookie | Removed at runtime by `cdn_cache_guard` |
//! | An error response being cached at the CDN or on the server | Never cached |
//! | Persisting data before knowing whose it is | Memory only until [`set_cache_owner`] |
//! | A shared function reading the viewer some other way (a global, a header, a query using the session) | **Not caught.** Only share functions whose answer comes from their arguments |
//! | A mutation that forgets to [`invalidate_cached`] a read it changed | **Not caught.** The read stays stale until the screen reopens or the app regains focus |
//! | A per-user answer in a `ServerCache` without the user in the key | **Not caught.** Put the user's id in `K` |
//! | Toggle-style mutations acting on stale state | **Not caught.** Prefer "set to" mutations |
//! | Missing CDN rule | **Not caught.** Nothing is cached at the CDN; harmless |
//!
//! # Platform features
//!
//! - `web`: the persistent store is IndexedDB.
//! - `mobile`: the persistent store is a redb file in the OS cache directory.
//! - `server`: `ServerCache`, `cdn_cache_guard` and the `server` and `cdn`
//! modules. Turns the client cache off: one server process renders for
//! every visitor, so nothing per-user may be kept there.
//!
//! With neither `web` nor `mobile`, the client cache works in memory only.
pub use cdn_cache_guard;
pub use ServerCache;
pub use ;
pub use cache_shared;
pub use ;
/// Support for the code [`cache_shared`] generates. Not public API.