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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The current repository proves the core shape:
- Typed router
- Runtime-scoped resources
- Resource reload and invalidation
- Resource cache phase 1 (staleTime, deduplicate)
- Independent executable actions
- Keyboard Enter default action
- Accessible Enter hints
Expand All @@ -39,17 +40,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.7 @intent-framework/dom@0.1.0-alpha.7 @intent-framework/router@0.1.0-alpha.7 @intent-framework/testing@0.1.0-alpha.7
pnpm add @intent-framework/core@0.1.0-alpha.8 @intent-framework/dom@0.1.0-alpha.8 @intent-framework/router@0.1.0-alpha.8 @intent-framework/testing@0.1.0-alpha.8
```

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

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

```txt
Current alpha: v0.1.0-alpha.7
Current alpha: v0.1.0-alpha.8
First public alpha: v0.1.0-alpha.0
GitHub Releases: https://github.com/intent-framework/intent/releases
```
Expand Down Expand Up @@ -346,7 +347,7 @@ Current limitations include:
* No native renderer yet
* No SSR story yet
* No backend persistence yet
* No real resource cache policy yet
* Resource cache is phase 1 (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.7 @intent-framework/dom@0.1.0-alpha.7 @intent-framework/testing@0.1.0-alpha.7
pnpm add @intent-framework/core@0.1.0-alpha.8 @intent-framework/dom@0.1.0-alpha.8 @intent-framework/testing@0.1.0-alpha.8
```

Or with npm:

```sh
npm install @intent-framework/core@0.1.0-alpha.7 @intent-framework/dom@0.1.0-alpha.7 @intent-framework/testing@0.1.0-alpha.7
npm install @intent-framework/core@0.1.0-alpha.8 @intent-framework/dom@0.1.0-alpha.8 @intent-framework/testing@0.1.0-alpha.8
```

The quickstart pins `0.1.0-alpha.7` so the examples match the APIs shown below.
The quickstart pins `0.1.0-alpha.8` so the examples match the APIs shown below.

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

Expand Down
24 changes: 12 additions & 12 deletions docs/Release-Readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Status

Seven public alpha releases have been published: `0.1.0-alpha.0` through `0.1.0-alpha.7`.
Eight public alpha releases have been published: `0.1.0-alpha.0` through `0.1.0-alpha.8`.

Five packages are under the `@intent-framework/*` scope, four of which are published to npm. The server package `@intent-framework/server` remains private and unpublished.

Expand All @@ -29,7 +29,7 @@ Packages under `examples/` (e.g., `web-basic`) are private and must not be publi

| Field | Value | Status |
|---|---|---|
| Current version | `0.1.0-alpha.7` | Acceptable for alpha |
| Current version | `0.1.0-alpha.8` | Acceptable for alpha |
| `main` | `./dist/index.js` | Exists after build |
| `module` | `./dist/index.js` | Exists after build |
| `types` | `./dist/index.d.ts` | Exists after build |
Expand All @@ -47,7 +47,7 @@ Packages under `examples/` (e.g., `web-basic`) are private and must not be publi

| Field | Value | Status |
|---|---|---|
| Current version | `0.1.0-alpha.7` | Acceptable for alpha |
| Current version | `0.1.0-alpha.8` | Acceptable for alpha |
| `main` | `./dist/index.js` | Exists after build |
| `module` | `./dist/index.js` | Exists after build |
| `types` | `./dist/index.d.ts` | Exists after build |
Expand All @@ -63,7 +63,7 @@ Packages under `examples/` (e.g., `web-basic`) are private and must not be publi

| Field | Value | Status |
|---|---|---|
| Current version | `0.1.0-alpha.7` | Acceptable for alpha |
| Current version | `0.1.0-alpha.8` | Acceptable for alpha |
| `main` | `./dist/index.js` | Exists after build |
| `module` | `./dist/index.js` | Exists after build |
| `types` | `./dist/index.d.ts` | Exists after build |
Expand All @@ -79,7 +79,7 @@ Packages under `examples/` (e.g., `web-basic`) are private and must not be publi

| Field | Value | Status |
|---|---|---|
| Current version | `0.1.0-alpha.7` | Acceptable for alpha |
| Current version | `0.1.0-alpha.8` | Acceptable for alpha |
| `main` | `./dist/index.js` | Exists after build |
| `module` | `./dist/index.js` | Exists after build |
| `types` | `./dist/index.d.ts` | Exists after build |
Expand Down Expand Up @@ -214,7 +214,7 @@ All post-publish steps use the `NPM_TOKEN` / `NODE_AUTH_TOKEN` from the `npm` en

### Publish cadence

- Currently publishing **alpha releases** (`0.1.0-alpha.0` through `0.1.0-alpha.7` so far)
- Currently publishing **alpha releases** (`0.1.0-alpha.0` through `0.1.0-alpha.8` so far)
- Use `Changesets` pre-release mode for alpha/beta
- Graduate to stable after API surface is settled and real-world usage begins

Expand Down Expand Up @@ -273,20 +273,20 @@ Do not manually create GitHub Releases.
- Manual Publish Alpha workflow (manual dispatch only)
- [x] NPM_TOKEN secret added to GitHub repository secrets
- [x] First alpha published (v0.1.0-alpha.0)
- [x] Subsequent alpha releases published (v0.1.0-alpha.1 through v0.1.0-alpha.7)
- [x] Subsequent alpha releases published (v0.1.0-alpha.1 through v0.1.0-alpha.8)

## Post-release verification

Published packages (latest alpha):

- `@intent-framework/core@0.1.0-alpha.7`
- `@intent-framework/dom@0.1.0-alpha.7`
- `@intent-framework/router@0.1.0-alpha.7`
- `@intent-framework/testing@0.1.0-alpha.7`
- `@intent-framework/core@0.1.0-alpha.8`
- `@intent-framework/dom@0.1.0-alpha.8`
- `@intent-framework/router@0.1.0-alpha.8`
- `@intent-framework/testing@0.1.0-alpha.8`

Verified:

- npm install smoke test passed for alpha.0 through alpha.7
- npm install smoke test passed for alpha.0 through alpha.8
- `alpha` dist-tag points to the latest published alpha version (set by workflow post-publish step)
- `latest` dist-tag points to the latest alpha (changeset behavior — publishes to `latest` when no regular release exists)
- Git tags (`@intent-framework/pkg@version`) are present on remote (pushed by workflow post-publish step)
Expand Down
27 changes: 25 additions & 2 deletions examples/resource-lifecycle/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Resource Lifecycle Example

A focused demonstration of the full resource lifecycle in Intent — autoLoad, manual load, reload, invalidation, stale detection, failed state, and action-driven invalidation.
A focused demonstration of the full resource lifecycle in Intent — autoLoad, manual load, reload, invalidation, stale detection, failed state, action-driven invalidation, and the cache options introduced in alpha.8.

## What it demonstrates

Expand All @@ -12,6 +12,8 @@ A focused demonstration of the full resource lifecycle in Intent — autoLoad, m
- `.invalidates(resource)` on an action — marks resources stale on success
- Resource "failed" status when a loader throws
- Route-driven resource load context
- `cache.staleTime` — resource transitions to stale automatically after a timeout (alpha.8)
- `cache.deduplicate` — concurrent loads share the same in-flight promise (alpha.8)

## Run

Expand All @@ -28,11 +30,32 @@ Open the local URL printed by Vite. Use the browser console to inspect resource
pnpm test
```

Tests cover every resource status transition: autoLoad, idle, pending, ready, stale, failed, and action-driven invalidation.
Tests cover every resource status transition: autoLoad, idle, pending, ready, stale, failed, action-driven invalidation, and the alpha.8 cache options.

## Inspect

- Watch the console log for resource load events and load counts
- Each reload increments the team version counter
- The "Broken save" action demonstrates a failed invalidation
- `cache.staleTime` on the `cachedTeam` resource auto-transitions to stale after 50ms
- `cache.deduplicate` on the `dedupeReport` resource shares concurrent load promises
- Test file (`src/ResourceDemo.test.ts`) shows the full resource API surface via `@intent-framework/testing`

## Cache options (alpha.8)

```ts
// staleTime — auto-stale after a timeout (ms)
const cachedTeam = $.resource("cachedTeam", {
load: async () => loadTeam(),
cache: { staleTime: 50 },
})

// deduplicate — share in-flight promise between concurrent calls
const dedupeReport = $.resource("dedupeReport", {
load: async () => loadReport(),
autoLoad: false,
cache: { deduplicate: true },
})
```

When no `cache` object is set, existing behavior is preserved (no deduplication, no time-based staleness).
38 changes: 36 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 } from "./ResourceDemo.js"
import { ResourceDemo, teamLoadCount, cachedTeamLoadCount, dedupeReportLoadCount } from "./ResourceDemo.js"

const testServices = {
route: { name: "demo", path: "/:id", params: { id: "team_1" } },
Expand Down Expand Up @@ -82,12 +82,46 @@ describe("ResourceDemo", () => {
}, { services: testServices as any })
})

it("cache.staleTime marks resource stale after timeout", async () => {
await testScreen(ResourceDemo, async app => {
const cached = app.resource("cachedTeam")
expect(cached.status()).toBe("ready")
expect(cached.stale()).toBe(false)
await new Promise(r => setTimeout(r, 100))
expect(cached.stale()).toBe(true)
expect(cached.status()).toBe("ready")
}, { services: testServices as any })
})

it("reload resets staleTime timer and clears stale", async () => {
await testScreen(ResourceDemo, async app => {
const cached = app.resource("cachedTeam")
await new Promise(r => setTimeout(r, 60))
expect(cached.stale()).toBe(true)
const before = cachedTeamLoadCount
await cached.reload()
expect(cachedTeamLoadCount).toBe(before + 1)
expect(cached.stale()).toBe(false)
}, { services: testServices as any })
})

it("cache.deduplicate shares in-flight promise", async () => {
await testScreen(ResourceDemo, async app => {
const report = app.resource("dedupeReport")
expect(report.status()).toBe("idle")
const before = dedupeReportLoadCount
await Promise.all([report.load(), report.load()])
expect(dedupeReportLoadCount).toBe(before + 1)
expect(report.status()).toBe("ready")
}, { services: testServices as any })
})

it("inspectScreen reports resources with status/stale/error", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const graph = runtime.graph
const resources = graph.resources
expect(resources).toHaveLength(3)
expect(resources).toHaveLength(5)
const teamRes = resources.find(r => r.name === "team")!
expect(teamRes.status).toBe("ready")
expect(teamRes.hasValue).toBe(true)
Expand Down
37 changes: 37 additions & 0 deletions examples/resource-lifecycle/src/ResourceDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { AppServices } from "./types.js"

export let teamLoadCount = 0
export let auditLogLoadCount = 0
export let cachedTeamLoadCount = 0
export let dedupeReportLoadCount = 0

export type Team = {
id: string
Expand Down Expand Up @@ -38,6 +40,29 @@ export const ResourceDemo = screen<AppServices>("Resource Demo", $ => {
},
})

const cachedTeam = $.resource("cachedTeam", {
load: async ({ route }) => {
cachedTeamLoadCount++
return {
id: route.params.id,
name: "Cached",
members: 5,
version: cachedTeamLoadCount,
} satisfies Team
},
cache: { staleTime: 50 },
})

const dedupeReport = $.resource("dedupeReport", {
load: async () => {
dedupeReportLoadCount++
await new Promise(r => setTimeout(r, 20))
return { summary: "ok" }
},
autoLoad: false,
cache: { deduplicate: true },
})

const reloadTeam = $.act("Reload team")
.does(async () => {
await team.reload()
Expand All @@ -63,11 +88,23 @@ export const ResourceDemo = screen<AppServices>("Resource Demo", $ => {
await auditLog.load()
})

const reloadCachedTeam = $.act("Reload cached team")
.does(async () => {
await cachedTeam.reload()
})

const loadDedupeReport = $.act("Load dedupe report")
.does(async () => {
await dedupeReport.load()
})

$.surface("main").contains(
reloadTeam,
invalidateTeam,
saveTeam,
brokenSave,
loadAuditLog,
reloadCachedTeam,
loadDedupeReport,
)
})