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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The current repository proves the core shape:
- Runtime-scoped resources
- Resource reload and invalidation
- Resource cache phase 2 (staleTime, deduplicate, cache.key, ResourceKey)
- Resource cache phase 3 (cacheTime — single-runtime stale value retention and eviction)
- Independent executable actions
- Keyboard Enter default action
- Accessible Enter hints
Expand All @@ -40,17 +41,17 @@ It is not production-ready yet. The goal right now is to keep the foundation sma
Intent is available as experimental alpha packages on npm:

```sh
pnpm add @intent-framework/core@0.1.0-alpha.9 @intent-framework/dom@0.1.0-alpha.9 @intent-framework/router@0.1.0-alpha.9 @intent-framework/testing@0.1.0-alpha.9
pnpm add @intent-framework/core@0.1.0-alpha.10 @intent-framework/dom@0.1.0-alpha.10 @intent-framework/router@0.1.0-alpha.10 @intent-framework/testing@0.1.0-alpha.10
```

```sh
npm install @intent-framework/core@0.1.0-alpha.9 @intent-framework/dom@0.1.0-alpha.9 @intent-framework/router@0.1.0-alpha.9 @intent-framework/testing@0.1.0-alpha.9
npm install @intent-framework/core@0.1.0-alpha.10 @intent-framework/dom@0.1.0-alpha.10 @intent-framework/router@0.1.0-alpha.10 @intent-framework/testing@0.1.0-alpha.10
```

The quickstart pins the current alpha version so examples match the published APIs.

```txt
Current alpha: v0.1.0-alpha.9
Current alpha: v0.1.0-alpha.10
First public alpha: v0.1.0-alpha.0
GitHub Releases: https://github.com/intent-framework/intent/releases
```
Expand Down Expand Up @@ -255,7 +256,7 @@ The repository includes five runnable examples, each demonstrating a different s
|---------|---------------------|
| [`canonical-invite`](examples/canonical-invite) | Minimal screen: text ask, primary action, validation, blocked reasons, feedback, `inspectScreen()`. Matches the Quickstart one-to-one. |
| [`choice-form`](examples/choice-form) | All state kinds (text, choice, boolean) and ask kinds (`asChoice()`, `asSecret()`, `hint()`), multiple surfaces per screen, flow diagnostics, full test coverage. |
| [`resource-lifecycle`](examples/resource-lifecycle) | Full resource lifecycle: autoLoad, manual load, reload, invalidation, stale detection, failed state, action-driven invalidation with `.invalidates()`. |
| [`resource-lifecycle`](examples/resource-lifecycle) | Full resource lifecycle: autoLoad, manual load, reload, invalidation, stale detection, failed state, action-driven invalidation with `.invalidates()`, staleTime, cache.key, cacheTime. |
| [`secret-vault`](examples/secret-vault) | Multi-step flows (3+ steps), `asSecret()` + `private()`, `BooleanState` toggles, multi-screen router app, flow diagnostics. |
| [`web-basic`](examples/web-basic) | Multi-screen team invite demo with router, resources, diagnostics panel, feedback, and keyboard default action. |

Expand Down Expand Up @@ -347,7 +348,7 @@ Current limitations include:
* No native renderer yet
* No SSR story yet
* No backend persistence yet
* Resource cache is phase 2 (staleTime, deduplicate, cache.key); cacheTime, swr, and cross-navigation cache are future
* Resource cache is phase 3 (staleTime, deduplicate, cache.key, cacheTime); swr and cross-navigation cache are future
* No DevTools package yet
* Automated version PR workflow and manual publish workflow are set up (Changesets configured)
* Demo side panels use manual DOM
Expand Down
6 changes: 3 additions & 3 deletions docs/Quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ This guide shows the fastest path from zero to a semantic screen using published
## 1. Install

```sh
pnpm add @intent-framework/core@0.1.0-alpha.9 @intent-framework/dom@0.1.0-alpha.9 @intent-framework/testing@0.1.0-alpha.9
pnpm add @intent-framework/core@0.1.0-alpha.10 @intent-framework/dom@0.1.0-alpha.10 @intent-framework/testing@0.1.0-alpha.10
```

Or with npm:

```sh
npm install @intent-framework/core@0.1.0-alpha.9 @intent-framework/dom@0.1.0-alpha.9 @intent-framework/testing@0.1.0-alpha.9
npm install @intent-framework/core@0.1.0-alpha.10 @intent-framework/dom@0.1.0-alpha.10 @intent-framework/testing@0.1.0-alpha.10
```

The quickstart pins `0.1.0-alpha.9` so the examples match the APIs shown below.
The quickstart pins `0.1.0-alpha.10` so the examples match the APIs shown below.

You also need `typescript` and `vitest` for type checking and tests.

Expand Down
8 changes: 4 additions & 4 deletions docs/agent/RELEASE-STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

| Package | Version | Published | Notes |
|---------|---------|-----------|-------|
| `@intent-framework/core` | `0.1.0-alpha.9` | Yes | alpha prerelease |
| `@intent-framework/dom` | `0.1.0-alpha.9` | Yes | alpha prerelease |
| `@intent-framework/router` | `0.1.0-alpha.9` | Yes | alpha prerelease |
| `@intent-framework/testing` | `0.1.0-alpha.9` | Yes | alpha prerelease |
| `@intent-framework/core` | `0.1.0-alpha.10` | Yes | alpha prerelease |
| `@intent-framework/dom` | `0.1.0-alpha.10` | Yes | alpha prerelease |
| `@intent-framework/router` | `0.1.0-alpha.10` | Yes | alpha prerelease |
| `@intent-framework/testing` | `0.1.0-alpha.10` | Yes | alpha prerelease |
| `@intent-framework/server` | `0.1.0` | No | private workspace package |

`@intent-framework/server` must remain `"private": true`. Never publish it.
Expand Down
5 changes: 4 additions & 1 deletion examples/resource-lifecycle/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</head>
<body>
<h1>Intent: Resource Lifecycle</h1>
<p>Demonstrates autoLoad, manual load, reload, invalidation, stale state, failed state, and action-driven invalidation.</p>
<p>Demonstrates autoLoad, manual load, reload, invalidation, stale state, failed state, action-driven invalidation, staleTime, cache.key, and cacheTime.</p>
<div id="root"></div>
<h2>Resource Status</h2>
<pre id="resource-status">Loading...</pre>
Expand All @@ -39,6 +39,9 @@ <h2>Semantic Graph (<code>inspectScreen()</code>)</h2>
<li>A failed loader produces <code>failed</code> status and error.</li>
<li>A failed action does not invalidate its targeted resources.</li>
<li>Route context is available to resource loaders.</li>
<li><code>cache.cacheTime</code> retains stale value until eviction timeout.</li>
<li>After <code>cacheTime</code> expiry, stale resource transitions from <code>ready</code> to <code>idle</code>.</li>
<li>Keyed resources each have independent <code>cacheTime</code> timers.</li>
</ul>
</div>
<script type="module" src="/src/main.ts"></script>
Expand Down
63 changes: 61 additions & 2 deletions examples/resource-lifecycle/src/ResourceDemo.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest"
import { testScreen } from "@intent-framework/testing"
import { createScreenRuntime, inspectScreen } from "@intent-framework/core"
import { ResourceDemo, teamLoadCount, cachedTeamLoadCount, dedupeReportLoadCount, keyedTeamLoadCount } from "./ResourceDemo.js"
import { ResourceDemo, teamLoadCount, cachedTeamLoadCount, dedupeReportLoadCount, keyedTeamLoadCount, timeEvictTeamLoadCount } from "./ResourceDemo.js"

const testServices = {
route: { name: "demo", path: "/:id", params: { id: "team_1" } },
Expand Down Expand Up @@ -157,6 +157,65 @@ describe("ResourceDemo", () => {
runtime.dispose()
})

// === cacheTime tests ===

it("invalidate keeps stale value before cacheTime expires", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const evict = runtime.resources.find(r => r.name === "timeEvictTeam")!
expect(evict.status).toBe("ready")
expect(evict.stale.current).toBe(false)
expect(evict.value).toBeDefined()
const val = evict.value
evict.invalidate()
expect(evict.stale.current).toBe(true)
expect(evict.value).toBe(val)
expect(evict.status).toBe("ready")
runtime.dispose()
})

it("cacheTime expiry resets active entry to idle", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const evict = runtime.resources.find(r => r.name === "timeEvictTeam")!
evict.invalidate()
expect(evict.status).toBe("ready")
expect(evict.stale.current).toBe(true)
await new Promise(r => setTimeout(r, 1100))
expect(evict.status).toBe("idle")
expect(evict.stale.current).toBe(false)
expect(evict.value).toBeUndefined()
runtime.dispose()
})

it("cacheTime works per key for keyed resource", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const evict = runtime.resources.find(r => r.name === "keyedTimeEvict")!
await evict.load({ route: { name: "demo", path: "/:id", params: { id: "team_b" } } })
expect(evict.status).toBe("ready")
evict.invalidate()
expect(evict.stale.current).toBe(true)
await new Promise(r => setTimeout(r, 1100))
expect(evict.status).toBe("idle")
expect(evict.stale.current).toBe(false)
runtime.dispose()
})

it("no-arg reload after cacheTime eviction reuses last context", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const evict = runtime.resources.find(r => r.name === "timeEvictTeam")!
const before = timeEvictTeamLoadCount
evict.invalidate()
await new Promise(r => setTimeout(r, 1100))
expect(evict.status).toBe("idle")
await evict.reload()
expect(timeEvictTeamLoadCount).toBe(before + 1)
expect(evict.status).toBe("ready")
runtime.dispose()
})

it("cache.key: no-arg reload reuses last active key", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
Expand All @@ -176,7 +235,7 @@ describe("ResourceDemo", () => {
await runtime.start()
const graph = runtime.graph
const resources = graph.resources
expect(resources).toHaveLength(6)
expect(resources).toHaveLength(8)
const teamRes = resources.find(r => r.name === "team")!
expect(teamRes.status).toBe("ready")
expect(teamRes.hasValue).toBe(true)
Expand Down
49 changes: 49 additions & 0 deletions examples/resource-lifecycle/src/ResourceDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export let auditLogLoadCount = 0
export let cachedTeamLoadCount = 0
export let dedupeReportLoadCount = 0
export let keyedTeamLoadCount = 0
export let timeEvictTeamLoadCount = 0
export let keyedTimeEvictLoadCount = 0

export type Team = {
id: string
Expand Down Expand Up @@ -77,6 +79,32 @@ export const ResourceDemo = screen<AppServices>("Resource Demo", $ => {
cache: { key: ({ route }) => route.params.id },
})

const timeEvictTeam = $.resource("timeEvictTeam", {
load: async ({ route }) => {
timeEvictTeamLoadCount++
return {
id: route.params.id ?? "default",
name: "TimeEvict",
members: 7,
version: timeEvictTeamLoadCount,
} satisfies Team
},
cache: { cacheTime: 1000 },
})

const keyedTimeEvict = $.resource("keyedTimeEvict", {
load: async ({ route }) => {
keyedTimeEvictLoadCount++
return {
id: route.params.id,
name: `Evict-${route.params.id}`,
members: 2,
version: keyedTimeEvictLoadCount,
} satisfies Team
},
cache: { key: ({ route }) => route.params.id, cacheTime: 1000 },
})

const reloadTeam = $.act("Reload team")
.does(async () => {
await team.reload()
Expand Down Expand Up @@ -125,6 +153,24 @@ export const ResourceDemo = screen<AppServices>("Resource Demo", $ => {
await keyedTeam.reload()
})

const invalidateTimeEvict = $.act("Invalidate time-evict team")
.does(() => {
timeEvictTeam.invalidate()
})

const loadKeyedTimeEvictB = $.act("Load keyed time-evict (team_b)")
.does(async ({ navigate }) => {
await keyedTimeEvict.load({
route: { name: "demo", path: "/:id", params: { id: "team_b" } },
navigate,
})
})

const invalidateKeyedTimeEvict = $.act("Invalidate keyed time-evict")
.does(() => {
keyedTimeEvict.invalidate()
})

$.surface("main").contains(
reloadTeam,
invalidateTeam,
Expand All @@ -135,5 +181,8 @@ export const ResourceDemo = screen<AppServices>("Resource Demo", $ => {
loadDedupeReport,
loadKeyedTeamB,
reloadKeyedTeam,
invalidateTimeEvict,
loadKeyedTimeEvictB,
invalidateKeyedTimeEvict,
)
})