Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/nine-snails-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@intent-framework/core": patch
---

feat(core): resource cacheTime — single-runtime stale-value eviction

Adds `cache.cacheTime` option to ResourceCacheOptions. When set, a stale entry's
value is retained for the specified milliseconds before being evicted. Active
entry eviction resets status to idle, clears value/error/stale, and notifies
subscribers. Inactive keyed entry eviction silently removes the entry. Works per
key, per ResourceNode, within a single ScreenRuntime. No cross-navigation
survival. No SWR.
48 changes: 42 additions & 6 deletions docs/Resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ On failure, `error` contains the error message and `status` is `"failed"`.

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

## Cache options (Phase 1 + Phase 2)
## Cache options (Phase 1 + Phase 2 + Phase 3)

Resources support optional `cache` configuration:

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

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

Scope limitations (Phase 2):
Scope limitations (Phase 2 + Phase 3):

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

### cacheTime (Phase 3)

`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).

`cacheTime` starts when an entry becomes stale — from `invalidate()`, action invalidation, or `staleTime` expiry.

Before `cacheTime` expires:
- `status` remains `"ready"`
- `value` remains available
- `stale` is `true`

When `cacheTime` expires (active entry eviction):
- `status` → `"idle"`
- `value` → `undefined`
- `error` → `undefined`
- `stale` → `false`
- Subscribers are notified

When `cacheTime` expires (inactive keyed entry eviction):
- Entry is removed from the internal map
- No subscriber notification

Behavior:
- **`load()`/`reload()`** clears the cacheTime timer, clears stale, starts a fresh load.
- **Successful load** clears the cacheTime timer (no longer stale).
- **Failed load** does NOT start cacheTime (preserves existing failure behavior).
- **`dispose()`** clears all cacheTime timers.
- **With `cache.key`**, each key has an independent cacheTime timer.
- **Works without `staleTime`** — manual `invalidate()` is sufficient to start cacheTime.
- **`cacheTime: 0`** evicts on the next event loop tick after staleness.

Scope limitations (Phase 3):
- **Single-runtime only.** cacheTime does not survive runtime disposal.
- **No SWR.** Eviction does not trigger automatic reload.
- **No cross-navigation cache.** Disposing the runtime clears all entries.

### deduplicate

`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:
Expand All @@ -354,7 +392,6 @@ When `cache` is not set at all, deduplication is disabled to preserve backward c

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

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

- **No global cache.** Each runtime owns its resource nodes. There is no shared cache between runtimes.
- **No `cacheTime`.** There is no time-based retention of stale values beyond `staleTime` transitions. Keyed entries persist for the node's lifetime.
- **No stale-while-revalidate (SWR).** Stale resources stay stale until explicitly reloaded.
- **No Suspense integration.** Resources do not integrate with React Suspense or any other framework's loading boundaries.
- **No server framework integration yet.** Resources live on screen definitions and runtimes. Server-side resource hydration is not implemented.
Expand Down
8 changes: 4 additions & 4 deletions docs/proposals/Resource-Cache-And-Stale-Semantics.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Resource Cache and Stale Semantics — Design Proposal

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

> **Phase 1** (PR #119, `@intent-framework/core@0.1.0-alpha.8`): `cache.staleTime` and `cache.deduplicate`.
> **Phase 2** (PR #123, `@intent-framework/core@0.1.0-alpha.9`): `cache.key` only, scoped to one runtime and one `ResourceNode`.
> **Phase 3** (PR #xxx, design): `cacheTime` single-runtime in-memory eviction, per `ResourceNode`, per key.
> **Phase 3** (PR #127, `@intent-framework/core@0.1.0-alpha.10`): `cacheTime` single-runtime in-memory eviction, per `ResourceNode`, per key.
> **Phase 4+**: SWR, cross-navigation cache store, dependency-tracked keys. These remain design-only until implementation begins.

---
Expand Down Expand Up @@ -764,7 +764,7 @@ When no `key` option is provided:

## Phase 3: CacheTime — Single-Runtime Design

**Status:** Design (not yet implemented)
**Status:** Design (PR #126), implemented in `@intent-framework/core@0.1.0-alpha.10` (PR #127)
**Scope:** `cacheTime` option, in-memory entry retention/eviction within the lifetime of a single `ResourceNode`. No cross-navigation cache store. No SWR. No dependency-tracked keys. No server resources.

### Revisiting Phase 2 Q7
Expand Down Expand Up @@ -1261,7 +1261,7 @@ No changeset is needed for this proposal — it is design-only.
| Backward compat | Full — all existing tests pass without modification |
| New exports | `ResourceKey` type only |

### Phase 3 (designed but not yet implemented)
### Phase 3 (implemented in `@intent-framework/core@0.1.0-alpha.10`, PR #127)

- **`cacheTime`** — single-runtime in-memory entry retention/eviction
- Per-key eviction within a single `ResourceNode`
Expand Down
Loading