Skip to content

Commit e8ecbfa

Browse files
committed
feat(core): resource cache phase 3 — cacheTime single-runtime eviction
1 parent 409bb62 commit e8ecbfa

5 files changed

Lines changed: 656 additions & 11 deletions

File tree

.changeset/nine-snails-walk.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@intent-framework/core": patch
3+
---
4+
5+
feat(core): resource cacheTime — single-runtime stale-value eviction
6+
7+
Adds `cache.cacheTime` option to ResourceCacheOptions. When set, a stale entry's
8+
value is retained for the specified milliseconds before being evicted. Active
9+
entry eviction resets status to idle, clears value/error/stale, and notifies
10+
subscribers. Inactive keyed entry eviction silently removes the entry. Works per
11+
key, per ResourceNode, within a single ScreenRuntime. No cross-navigation
12+
survival. No SWR.

docs/Resources.md

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ On failure, `error` contains the error message and `status` is `"failed"`.
251251

252252
See the [Inspect Screen and Diagnostics Guide](Inspect-Screen.md) for more detail.
253253

254-
## Cache options (Phase 1 + Phase 2)
254+
## Cache options (Phase 1 + Phase 2 + Phase 3)
255255

256256
Resources support optional `cache` configuration:
257257

@@ -260,7 +260,8 @@ const team = $.resource("team", {
260260
load: async ({ route }) => loadTeam(route.params.teamId),
261261
cache: {
262262
key: ({ route }) => route.params.teamId, // optional, derive cache key from context
263-
staleTime: 5_000, // ms (optional, default: Infinity)
263+
staleTime: 5_000, // ms (optional, default: undefined)
264+
cacheTime: 30_000, // ms (optional, default: undefined, no time-based eviction)
264265
deduplicate: true, // boolean (optional, default: true when cache is set)
265266
},
266267
})
@@ -275,6 +276,7 @@ const team = $.resource("team", {
275276
- status (idle/pending/ready/failed)
276277
- stale flag
277278
- staleTime timer
279+
- cacheTime timer
278280
- in-flight promise (for deduplication)
279281

280282
The `ResourceRef` always reflects the **active key** entry — the key from the most recent `load()` or `reload()` call.
@@ -309,12 +311,12 @@ Key semantics:
309311
- `invalidate()` marks only the active entry stale.
310312
- `cache.deduplicate` deduplicates per active key.
311313
- `cache.staleTime` timers are per key.
314+
- `cache.cacheTime` timers are per key.
312315
- Resources without `cache.key` behave exactly as before (single-entry behavior).
313316

314-
Scope limitations (Phase 2):
317+
Scope limitations (Phase 2 + Phase 3):
315318

316319
- **Single-runtime only.** Keyed entries live in one `ResourceNode` within one `ScreenRuntime`.
317-
- **No `cacheTime`.** Entries persist for the node's lifetime. No time-based eviction.
318320
- **No SWR.** Stale data must be explicitly reloaded.
319321
- **No cross-navigation cache.** Disposing the runtime clears all entries.
320322
- **No dependency-tracked keys.** Key is derived from context at load time; automatically reacting to context changes is future work.
@@ -336,6 +338,42 @@ Behavior:
336338
- **`dispose()`** clears all stale timers and prevents late notifications.
337339
- **With `cache.key`**, each key has an independent stale timer.
338340

341+
### cacheTime (Phase 3)
342+
343+
`cache.cacheTime` is an optional number in milliseconds controlling how long a stale entry's value is retained before eviction. Default is `undefined` (no time-based eviction).
344+
345+
`cacheTime` starts when an entry becomes stale — from `invalidate()`, action invalidation, or `staleTime` expiry.
346+
347+
Before `cacheTime` expires:
348+
- `status` remains `"ready"`
349+
- `value` remains available
350+
- `stale` is `true`
351+
352+
When `cacheTime` expires (active entry eviction):
353+
- `status``"idle"`
354+
- `value``undefined`
355+
- `error``undefined`
356+
- `stale``false`
357+
- Subscribers are notified
358+
359+
When `cacheTime` expires (inactive keyed entry eviction):
360+
- Entry is removed from the internal map
361+
- No subscriber notification
362+
363+
Behavior:
364+
- **`load()`/`reload()`** clears the cacheTime timer, clears stale, starts a fresh load.
365+
- **Successful load** clears the cacheTime timer (no longer stale).
366+
- **Failed load** does NOT start cacheTime (preserves existing failure behavior).
367+
- **`dispose()`** clears all cacheTime timers.
368+
- **With `cache.key`**, each key has an independent cacheTime timer.
369+
- **Works without `staleTime`** — manual `invalidate()` is sufficient to start cacheTime.
370+
- **`cacheTime: 0`** evicts on the next event loop tick after staleness.
371+
372+
Scope limitations (Phase 3):
373+
- **Single-runtime only.** cacheTime does not survive runtime disposal.
374+
- **No SWR.** Eviction does not trigger automatic reload.
375+
- **No cross-navigation cache.** Disposing the runtime clears all entries.
376+
339377
### deduplicate
340378

341379
`cache.deduplicate` is an optional boolean. When `true`, concurrent `load()` and `reload()` calls while a load is already pending share the same in-flight promise:
@@ -354,7 +392,6 @@ When `cache` is not set at all, deduplication is disabled to preserve backward c
354392

355393
The following cache options remain as future work and are **not yet supported**:
356394

357-
- **`cacheTime`** — retention period for stale values before eviction
358395
- **`swr`** — stale-while-revalidate background refreshing
359396
- **Cross-navigation cache store** — persistent cache across screen navigations
360397
- **Dependency-tracked keys** — automatic key derivation and reload when context changes without explicit load/reload
@@ -364,7 +401,6 @@ The following cache options remain as future work and are **not yet supported**:
364401
Resources are intentionally limited:
365402

366403
- **No global cache.** Each runtime owns its resource nodes. There is no shared cache between runtimes.
367-
- **No `cacheTime`.** There is no time-based retention of stale values beyond `staleTime` transitions. Keyed entries persist for the node's lifetime.
368404
- **No stale-while-revalidate (SWR).** Stale resources stay stale until explicitly reloaded.
369405
- **No Suspense integration.** Resources do not integrate with React Suspense or any other framework's loading boundaries.
370406
- **No server framework integration yet.** Resources live on screen definitions and runtimes. Server-side resource hydration is not implemented.

docs/proposals/Resource-Cache-And-Stale-Semantics.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# Resource Cache and Stale Semantics — Design Proposal
22

3-
**Status:** Phase 1 implemented (staleTime + deduplicate), Phase 2 implemented (cache.key), Phase 3 designed (cacheTime single-runtime)
3+
**Status:** Phase 1 implemented (staleTime + deduplicate), Phase 2 implemented (cache.key), Phase 3 implemented (cacheTime single-runtime)
44
**Date:** 2026-06-27
55
**Author:** Big Pickle
66
**Affected package:** `@intent-framework/core`
77
**Related docs:** `docs/Resources.md`, `docs/Specification.md`
88

99
> **Phase 1** (PR #119, `@intent-framework/core@0.1.0-alpha.8`): `cache.staleTime` and `cache.deduplicate`.
1010
> **Phase 2** (PR #123, `@intent-framework/core@0.1.0-alpha.9`): `cache.key` only, scoped to one runtime and one `ResourceNode`.
11-
> **Phase 3** (PR #xxx, design): `cacheTime` single-runtime in-memory eviction, per `ResourceNode`, per key.
11+
> **Phase 3** (PR #126, `@intent-framework/core@0.1.0-alpha.10`): `cacheTime` single-runtime in-memory eviction, per `ResourceNode`, per key.
1212
> **Phase 4+**: SWR, cross-navigation cache store, dependency-tracked keys. These remain design-only until implementation begins.
1313
1414
---
@@ -1261,7 +1261,7 @@ No changeset is needed for this proposal — it is design-only.
12611261
| Backward compat | Full — all existing tests pass without modification |
12621262
| New exports | `ResourceKey` type only |
12631263

1264-
### Phase 3 (designed but not yet implemented)
1264+
### Phase 3 (implemented in `@intent-framework/core@0.1.0-alpha.10`)
12651265

12661266
- **`cacheTime`** — single-runtime in-memory entry retention/eviction
12671267
- Per-key eviction within a single `ResourceNode`

0 commit comments

Comments
 (0)