Skip to content

Commit 5515d14

Browse files
committed
fix(cache): return stale cache data instead of null for stale-while-revalidate
getValidCacheData discarded cached entries when stale, dropping data to null and crashing consumers that expected data to remain present during background revalidation. The isStale guard now only influences whether a refetch is triggered (via getStaleStatus), not whether existing data is shown to the component. Made-with: Cursor
1 parent 408cca1 commit 5515d14

3 files changed

Lines changed: 170 additions & 106 deletions

File tree

.cursor/rules/tutorials.mdc

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ globs: documentation/blog/tutorials/**
55

66
# Tutorial blog posts
77

8-
Every tutorial blog post **must** include a version badge immediately after the frontmatter, before any other content.
9-
Use the Docusaurus `:::info` admonition:
8+
## Structure: intro, truncate, version, content
9+
10+
Every tutorial follows this layout:
11+
12+
1. **Intro paragraph** (before `{/* truncate */}`): a short, compelling hook that makes the reader want to click
13+
through. This is what appears in the blog listing. Focus on the problem and what the reader will gain.
14+
2. **`{/* truncate */}`**: Docusaurus truncation marker.
15+
3. **Version badge** (after truncate): a `:::info Version` admonition stating the Hyper Fetch version.
16+
4. **Tutorial content**: the actual tutorial sections.
1017

1118
```mdx
1219
---
@@ -17,10 +24,84 @@ tags: [Feature, TypeScript]
1724
date: 2026-05-01
1825
---
1926

27+
A compelling intro paragraph that hooks the reader. What problem does this solve?
28+
Why should they care? Keep it to 2-3 sentences max.
29+
30+
{/* truncate */}
31+
2032
:::info Version
2133
This tutorial was written for **Hyper Fetch X.Y**.
2234
:::
35+
36+
## First section
37+
...
2338
```
2439

2540
The version should refer to the latest major (or major.minor) Hyper Fetch release that the tutorial content applies to.
2641
This helps readers know whether the tutorial matches their installed version.
42+
43+
## Title conventions
44+
45+
- Titles must be **short and descriptive** (ideally 3-7 words).
46+
- Avoid subtitles, colons with long elaborations, or keyword-stuffing.
47+
- Good: "SDK Configuration", "Socket SDK Basics", "Testing with SDK Mocks"
48+
- Bad: "SDK Configuration: Method-Specific Defaults, Functions, and the Socket SDK"
49+
50+
## Writing quality
51+
52+
- Tutorials must be **concise and concrete**. Every paragraph should earn its place.
53+
- Write in a direct, technical style. No filler, no fluff.
54+
- **No AI quirks**: avoid em-dashes ("—"), "Let's dive in", "In this tutorial, we will explore...",
55+
"This is particularly powerful", "Fear not", and similar cliches.
56+
- **Empathetic, not provocative**. Describe real problems developers face, but don't lecture or blame.
57+
Avoid "you grep and hope" or "your code is scattered". Instead, acknowledge the difficulty and show how
58+
the feature helps. Good: "As the number of endpoints grows, keeping configuration consistent gets harder."
59+
Bad: "Your config is a mess spread across 50 files."
60+
- Use short sentences. Prefer active voice.
61+
- Avoid restating what the code already shows. If the code is self-explanatory, don't narrate it.
62+
63+
## Content structure
64+
65+
1. **Open with the problem**. What does the reader gain? What pain does this solve? Be concrete and specific.
66+
2. **Show code early**. Readers come for working examples, not prose.
67+
3. **Keep sections focused**. One concept per section. Split with `---` between `#` and `##` headings.
68+
4. **End with links** to relevant docs, not a summary paragraph that repeats the intro.
69+
70+
## Interactive examples
71+
72+
- Use `live` code blocks (`` ```tsx live `` or `` ```ts live ``) when interactivity helps the reader understand behavior.
73+
- Live examples should be minimal and focused on a single concept.
74+
- Global scope variables are available without imports; check:
75+
- `documentation/src/theme/CodeBlock/live-code-block/playground/global-scope.ts`
76+
- `documentation/src/theme/CodeBlock/live-code-block/playground/create-global-requests.ts`
77+
- Do not overuse live examples. One or two per tutorial is enough. Static code blocks are fine for most things.
78+
79+
## Mermaid diagrams
80+
81+
- Use mermaid diagrams when a concept benefits from visual explanation (flows, lifecycles, ordering).
82+
- **0-2 per post**. Not every tutorial needs one. Only add a diagram when it genuinely clarifies something
83+
that text or code alone cannot.
84+
- Timelines work well for lifecycles and ordered steps. Flowcharts and sequence diagrams work for
85+
request/response flows or decision logic.
86+
- Keep them small and readable. If a diagram has more than ~8 nodes, simplify it.
87+
88+
Example (timeline):
89+
90+
```mermaid
91+
timeline
92+
title Request Lifecycle
93+
1. Pre-Request : Deduplication : Queueing : Interceptors
94+
2. Request : onRequestStart() : onUploadProgress() : onRequestEnd()
95+
3. Response : onResponseStart() : onDownloadProgress() : onResponseEnd()
96+
4. Result : Interceptors : onSuccess() : onError() : onFinish()
97+
```
98+
99+
## Code examples
100+
101+
- Keep examples **short**. Long blocks are hard to scan.
102+
- **Use inline comments to explain intent, configs, and setup**. Comments help readers understand *why*
103+
something is configured a certain way, not just *what* the code does. Good: `// Not idempotent, should not retry`.
104+
Bad: `// Set retry to 0`. Skip comments on lines that are already self-explanatory.
105+
- Use highlighting (`// highlight-start`, `// highlight-next-line`) to draw attention to the key parts.
106+
- Use diffs (`// diff-add-start`, `// diff-remove-start`) when showing before/after changes.
107+
- Every example must be accurate against the current implementation in `/packages`.

documentation/blog/tutorials/2026-04-29-sdk-configuration.mdx

Lines changed: 84 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,206 +1,189 @@
11
---
22
slug: sdk-configuration
3-
title: "SDK Configuration: Method-Specific Defaults, Functions, and the Socket SDK"
3+
title: "SDK Configuration"
44
authors: [maciej]
55
tags: [SDK, Sockets, Configuration, TypeScript]
66
date: 2026-04-29
77
---
88

9-
:::info Version
10-
This tutorial was written for **Hyper Fetch 8.0**.
11-
:::
12-
13-
> Hyper Fetch SDK configuration gives you method-level precision, full access to every Request setter through
14-
> function-based values, and a Socket SDK that brings the same developer experience to real-time communication.
9+
Your API schema generates a fully typed SDK, but the generated code only knows about endpoints and types. Real projects
10+
still need caching policies, retry rules, auth settings, and response mappers. `$configure` lets you inject all of that
11+
into the SDK in one place, so every request comes pre-configured the moment you access it. It also makes testing easier
12+
by letting you swap in mocks without tools like `msw` or `nock`.
1513

1614
{/* truncate */}
1715

18-
## Precise Configuration with Dot-Path Keys
16+
:::info Version This tutorial was written for **Hyper Fetch 8.0**. :::
17+
18+
## Dot-path keys
1919

20-
The `$configure` API accepts **dot-path keys** that mirror the SDK's property chain. Since the SDK uses
21-
`sdk.users.$get` to access a request, you configure it with `"users.$get"`:
20+
The SDK uses property chains like `sdk.users.$get`. Configuration keys mirror that path:
2221

2322
```typescript
2423
const configured = sdk.$configure({
24+
// Cache the user list for 30s
2525
"users.$get": { cache: true, cacheTime: 30000 },
26+
// Not idempotent, should not retry
2627
"users.$post": { retry: 0 },
28+
// Serve stale profile data while revalidating in background
2729
"users.$userId.$get": { staleTime: 5000 },
2830
});
2931
```
3032

31-
`"users.$get"` only affects `sdk.users.$get` — not `sdk.users.$post`, not `sdk.users.$userId.$get`. Each key maps
32-
to exactly one request.
33+
`"users.$get"` targets exactly `sdk.users.$get`. It does not affect `$post` or nested routes.
3334

34-
Endpoint-group keys like `"/users"` or `"/users/*"` are also supported for applying broad defaults across all methods
35-
on an endpoint.
35+
To apply settings across an entire resource, use endpoint-group keys:
36+
37+
```typescript
38+
const configured = sdk.$configure({
39+
// All /users endpoints get caching
40+
"/users": { cache: true },
41+
// Admin routes require auth, no retry on privileged actions
42+
"/admin/*": { auth: true, retry: 0 },
43+
});
44+
```
3645

3746
---
3847

39-
## Function-Based Values
48+
## Function values
4049

41-
Plain objects cover simple settings, but `$configure` also accepts **functions**. A function receives the Request
42-
instance and returns a modified one, giving you access to the full API:
50+
Plain objects cover simple settings. When you need mappers or conditional logic, pass a function that receives the
51+
`Request` instance and returns a modified one:
4352

4453
```typescript
4554
const configured = sdk.$configure({
46-
// Plain object for simple settings
55+
// Base retry for all requests
4756
"*": { retry: 3 },
4857

49-
// Function for advanced configuration
58+
// highlight-start
59+
// Normalize the API's snake_case into camelCase for the frontend
5060
"users.$get": (request) =>
51-
request
52-
.setResponseMapper(userListMapper)
53-
.setCache(true)
54-
.setCacheTime(30000),
55-
56-
// Mocking for tests
57-
"auth.login.$post": (request) =>
58-
request.setMock(() => ({
59-
data: { token: "test-token" },
60-
error: null,
61-
status: 200,
62-
})),
61+
request.setResponseMapper(snakeToCamelMapper).setCache(true).setCacheTime(30000),
62+
// highlight-end
63+
64+
// Attach the org header that the billing API requires
65+
"billing.invoices.$get": (request) =>
66+
request.setHeaders({ "X-Org-Id": getCurrentOrgId() }),
6367
});
6468
```
6569

66-
This is particularly powerful for testing. Instead of mocking at the network level, you can create a test SDK with all
67-
endpoints mocked through `$configure`:
70+
Every `Request` setter is available: mappers, headers, auth, mocks, interceptors.
71+
72+
### SDK-level mocks for testing
73+
74+
Instead of maintaining a separate network mock layer, mock directly on the SDK:
6875

6976
```typescript
7077
export const testSdk = sdk.$configure({
71-
"users.$get": (request) => request.setMock(() => ({ data: mockUsers })),
72-
"users.$userId.$get": (request) => request.setMock(() => ({ data: mockUser })),
73-
"posts.$get": (request) => request.setMock(() => ({ data: [] })),
78+
"users.$get": (req) => req.setMock(() => ({ data: [{ id: 1, name: "Alice" }] })),
79+
"users.$userId.$get": (req) => req.setMock(() => ({ data: { id: 1, name: "Alice" } })),
80+
"billing.invoices.$get": (req) => req.setMock(() => ({ data: [] })),
7481
});
7582
```
7683

84+
Import `testSdk` instead of `sdk` in your test setup. No interceptors, no service workers, no polyfills.
85+
7786
---
7887

79-
## 3-Level Application Order
88+
## Application order
89+
90+
When multiple keys match a single request, they stack in a fixed order:
8091

81-
Configurations stack deterministically. When multiple keys match, they apply in this order:
92+
```mermaid
93+
timeline
94+
title Configuration layers
95+
1. Global : "*" : Applied first, base defaults
96+
2. Endpoint groups : "/users", "/admin/*" : Domain-level policies
97+
3. Dot-path keys : "users.$get" : Per-request overrides, wins on conflict
98+
```
8299

83-
1. **`"*"`** — Global defaults (applied first)
84-
2. **Endpoint groups**`"/users"`, `"/users/*"` (applied second)
85-
3. **Dot-path keys**`"users.$get"` (applied last, wins on conflict)
100+
Each level preserves settings from earlier levels unless it explicitly overrides them.
86101

87102
```typescript
88103
const configured = sdk.$configure({
104+
// Retry everything 3 times by default
89105
"*": { retry: 3 },
106+
// Cache all user-related endpoints
90107
"/users": { cache: true },
108+
// Deduplicate only the user list fetch
91109
"users.$get": { deduplicate: true },
92110
});
93111

94112
const request = configured.users.$get;
95-
// retry: 3 (from "*") + cache: true (from "/users") + deduplicate: true (from "users.$get")
96-
// All three stack. Later levels only override properties they explicitly set.
113+
// Result: retry: 3 + cache: true + deduplicate: true
97114
```
98115

99-
Global defaults are the foundation, endpoint groups add domain-specific settings on top, and method-specific keys
100-
fine-tune individual requests. Properties from earlier levels are preserved unless a later level explicitly overwrites
101-
them.
116+
Set global defaults once, add domain-level policies per resource, and fine-tune individual endpoints only when needed.
102117

103118
---
104119

105120
## Socket SDK
106121

107-
The SDK pattern extends to `@hyper-fetch/sockets`. The **Socket SDK** works exactly like the HTTP SDK, but uses
108-
`$listener` and `$emitter` as leaf keys instead of HTTP methods:
122+
`@hyper-fetch/sockets` uses the same pattern. Leaf keys are `$listener` and `$emitter` instead of HTTP methods:
109123

110124
```typescript
111125
import { createSocketSdk } from "@hyper-fetch/sockets";
112126

113127
const sdk = createSocketSdk<typeof socket, MyChatSchema>(socket);
114128

115-
// Listen to events
116129
sdk.chat.messages.$listener.listen(({ data }) => {
117-
console.log(data.text, data.user);
130+
appendMessage(data.text, data.user);
118131
});
119132

120-
// Emit events
121133
sdk.chat.messages.$emitter.emit({ payload: { text: "Hello!" } });
122-
123-
// Dynamic topics with parameters
124-
sdk.chat.$roomId.$listener
125-
.setParams({ roomId: "general" })
126-
.listen(({ data }) => console.log(data));
127134
```
128135

129-
### Socket SDK Configuration
130-
131-
The Socket SDK supports the same `$configure` pattern with full parity:
136+
Configuration uses the same 3-level order and supports both objects and functions:
132137

133138
```typescript
134139
const configured = sdk.$configure({
135-
// Global — applies to all listeners and emitters
140+
// Auto-reconnect all socket listeners
136141
"*": { options: { reconnect: true } },
137-
138-
// Topic group — applies to everything under chat/*
142+
// Prioritize chat topic delivery
139143
"chat/*": { options: { priority: "high" } },
140-
141-
// Instance-specific — targets one emitter (type-narrowed, no cast needed)
142-
"chat.messages.$emitter": (instance) => instance.setPayloadMapper(messageMapper),
144+
// highlight-next-line
145+
// Sanitize outgoing messages before sending
146+
"chat.messages.$emitter": (instance) => instance.setPayloadMapper(sanitizeMessage),
143147
});
144148
```
145149

146-
The same 3-level application order applies: `"*"` (global) -> topic groups (`"chat/*"`) -> dot-path
147-
(`"chat.messages.$listener"`). Both plain-object shorthand and function-based values work. Dot-path keys narrow
148-
the callback parameter to the exact instance type — `$listener` keys receive `ListenerInstance`, `$emitter` keys
150+
Dot-path keys narrow the callback type automatically: `$listener` keys receive `ListenerInstance`, `$emitter` keys
149151
receive `EmitterInstance`.
150152

151153
---
152154

153-
## Multi-File Configuration
155+
## Splitting config across files
154156

155-
For large projects, split configuration by domain using `createConfiguration` (HTTP) or
156-
`createSocketConfiguration` (sockets):
157+
When your app grows past a dozen endpoints, split configuration by domain:
157158

158159
```typescript title="config/users.ts"
159160
import { createConfiguration } from "@hyper-fetch/core";
160161

161162
export const usersConfig = createConfiguration<ApiSchema>()({
163+
// Normalize and cache the user list
162164
"users.$get": (request) =>
163-
request.setResponseMapper(userListMapper).setCache(true),
165+
request.setResponseMapper(snakeToCamelMapper).setCache(true),
166+
// Require auth, allow stale data for 5s on profile pages
164167
"users.$userId.$get": (request) =>
165168
request.setAuth(true).setStaleTime(5000),
166169
});
167170
```
168171

169-
```typescript title="config/chat.ts"
170-
import { createSocketConfiguration } from "@hyper-fetch/sockets";
171-
172-
export const chatConfig = createSocketConfiguration<ChatSchema>()({
173-
"chat/*": { options: { reconnect: true } },
174-
"chat.messages.$listener": (instance) =>
175-
instance.setOptions({ buffer: true }),
172+
```typescript title="config/billing.ts"
173+
export const billingConfig = createConfiguration<ApiSchema>()({
174+
// Cache invoices for 1 minute
175+
"billing.invoices.$get": { cache: true, cacheTime: 60000 },
176+
// Never retry invoice creation
177+
"billing.invoices.$post": { retry: 0 },
176178
});
177179
```
178180

179181
```typescript title="sdk.ts"
180-
export const api = createSdk(client)
181-
.$configure(usersConfig);
182-
183-
export const ws = createSocketSdk(socket)
184-
.$configure(chatConfig);
182+
export const api = createSdk(client).$configure(usersConfig).$configure(billingConfig);
185183
```
186184

187-
Keys are validated against your schema at compile time — typos are caught immediately.
188-
189-
---
190-
191-
## How It Works Under the Hood
192-
193-
The proxy that powers both SDKs tracks the **full accessor path** (e.g., `["users", "$get"]`) as you traverse the
194-
chain. When a leaf node is reached, the proxy joins this path into a dot-separated string and passes it to
195-
`applyDefaults`. The defaults function sorts all configuration entries into buckets by type, applies them in the
196-
correct order, and returns the fully configured instance.
197-
198-
For sockets, the proxy also constructs the **topic string** from the path segments (converting `$paramName` keys to
199-
`:paramName` and applying camelCase-to-kebab-case), which enables topic group matching alongside dot-path matching.
200-
201-
The type system uses recursive conditional types (`ExtractSdkPaths`, `ExtractSocketTopics`, `TopicPrefixes`,
202-
`ResolveDotPath`) to derive the exact set of valid configuration keys from your schema and narrow callback parameters
203-
to the correct instance type, providing full autocomplete and compile-time validation.
185+
Keys are validated against your schema at compile time. A typo in `"users.$gett"` fails the build, not a production
186+
request.
204187

205188
---
206189

0 commit comments

Comments
 (0)