From b2bde22422be744e948b84c1c045aefea81e1c22 Mon Sep 17 00:00:00 2001 From: Mahyar Date: Sat, 27 Jun 2026 09:57:12 +0330 Subject: [PATCH] =?UTF-8?q?docs:=20update=20alpha.7=E2=86=92alpha.8=20refe?= =?UTF-8?q?rences=20and=20add=20resource=20cache=20phase=201=20to=20resour?= =?UTF-8?q?ce-lifecycle=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++-- docs/Quickstart.md | 6 +-- docs/Release-Readiness.md | 24 ++++++------ examples/resource-lifecycle/README.md | 27 ++++++++++++- .../src/ResourceDemo.test.ts | 38 ++++++++++++++++++- .../resource-lifecycle/src/ResourceDemo.ts | 37 ++++++++++++++++++ 6 files changed, 118 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 4cbe848..9ea3cfd 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 ``` @@ -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 diff --git a/docs/Quickstart.md b/docs/Quickstart.md index eaa3b84..d8f13cf 100644 --- a/docs/Quickstart.md +++ b/docs/Quickstart.md @@ -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. diff --git a/docs/Release-Readiness.md b/docs/Release-Readiness.md index f3549b0..13ea3ee 100644 --- a/docs/Release-Readiness.md +++ b/docs/Release-Readiness.md @@ -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. @@ -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 | @@ -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 | @@ -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 | @@ -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 | @@ -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 @@ -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) diff --git a/examples/resource-lifecycle/README.md b/examples/resource-lifecycle/README.md index a3d0c4e..5388275 100644 --- a/examples/resource-lifecycle/README.md +++ b/examples/resource-lifecycle/README.md @@ -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 @@ -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 @@ -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). diff --git a/examples/resource-lifecycle/src/ResourceDemo.test.ts b/examples/resource-lifecycle/src/ResourceDemo.test.ts index 13a9fcb..39487c9 100644 --- a/examples/resource-lifecycle/src/ResourceDemo.test.ts +++ b/examples/resource-lifecycle/src/ResourceDemo.test.ts @@ -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" } }, @@ -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) diff --git a/examples/resource-lifecycle/src/ResourceDemo.ts b/examples/resource-lifecycle/src/ResourceDemo.ts index ac3af32..79f609c 100644 --- a/examples/resource-lifecycle/src/ResourceDemo.ts +++ b/examples/resource-lifecycle/src/ResourceDemo.ts @@ -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 @@ -38,6 +40,29 @@ export const ResourceDemo = screen("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() @@ -63,11 +88,23 @@ export const ResourceDemo = screen("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, ) })