Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8732f56
feat(rsc): demonstrate encrypted cache captures
hi-ogawa Aug 5, 2026
b42a63c
refactor(rsc): split protected cache captures example
hi-ogawa Aug 5, 2026
b63b4eb
Merge origin/main into fix/issue-1393
hi-ogawa Aug 5, 2026
cfcc4d2
chore(rsc): order protected captures route last
hi-ogawa Aug 5, 2026
38d2d81
refactor(rsc): render capture selector on server
hi-ogawa Aug 5, 2026
f572f1a
test(rsc): verify protected captures across reload
hi-ogawa Aug 5, 2026
c152ca9
nit
hi-ogawa Aug 5, 2026
7612a59
docs(rsc): explain protected capture argument adapter
hi-ogawa Aug 5, 2026
77d4c32
nit
hi-ogawa Aug 5, 2026
144a2b7
Merge origin/main into fix/issue-1393
hi-ogawa Aug 5, 2026
9e66ba2
fix(rsc): stabilize encrypted form cache keys
hi-ogawa Aug 5, 2026
56bdff7
docs(rsc): explain form cache key divergence
hi-ogawa Aug 5, 2026
da14545
test(rsc): document encrypted form reload miss
hi-ogawa Aug 5, 2026
2ed49ab
refactor(rsc): keep cache capture envelope synchronous
hi-ogawa Aug 5, 2026
cd72fec
refactor(rsc): clarify protected cache key encoding
hi-ogawa Aug 5, 2026
add3c5c
nit
hi-ogawa Aug 5, 2026
9b799ad
refactor(rsc): name cache capture encryption explicitly
hi-ogawa Aug 5, 2026
329abcb
nit
hi-ogawa Aug 5, 2026
5c0734e
refactor(rsc): remove cache capture key marker
hi-ogawa Aug 5, 2026
f45d329
nit
hi-ogawa Aug 5, 2026
09400ad
docs(rsc): note duplicate capture decryption
hi-ogawa Aug 5, 2026
2f70fe9
refactor(rsc): decrypt cache captures once
hi-ogawa Aug 5, 2026
8e36cae
Merge origin/main into refactor/use-cache-single-decode
hi-ogawa Aug 5, 2026
91574ae
Merge origin/main into refactor/use-cache-single-decode
hi-ogawa Aug 6, 2026
b295216
refactor(rsc): reuse decoded cache arguments
hi-ogawa Aug 6, 2026
78f8b6a
docs(rsc): clarify cache capture decode boundary
hi-ogawa Aug 6, 2026
c8bcd7b
docs(rsc): document callable cache example
hi-ogawa Aug 6, 2026
102635d
docs(rsc): expand use cache example guides
hi-ogawa Aug 6, 2026
b31de01
Merge origin/main into docs/use-cache-callable-readme
hi-ogawa Aug 6, 2026
c39ee3f
nit
hi-ogawa Aug 6, 2026
1ff663a
nit
hi-ogawa Aug 6, 2026
c93c12c
docs(rsc): illustrate use cache transforms
hi-ogawa Aug 6, 2026
a52860d
nit
hi-ogawa Aug 6, 2026
5e4020a
Merge origin/main into docs/use-cache-callable-readme
hi-ogawa Aug 6, 2026
22e8837
docs(rsc): align cache guide with routes
hi-ogawa Aug 6, 2026
f66e590
nit
hi-ogawa Aug 6, 2026
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
4 changes: 2 additions & 2 deletions packages/plugin-rsc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ npm create vite@latest -- --template rsc
**Integration examples:**

- [`./examples/basic`](./examples/basic) - Comprehensive showcase of standard RSC features and the primary E2E test fixture.
- [`./examples/use-cache`](./examples/use-cache) - Minimal cache feature inspired by Next.js's `"use cache"`, built with generic transform and RSC runtime APIs.
- [`./examples/use-cache-callable`](./examples/use-cache-callable) - Inline cache wrapper exported as a callable Server Function through a custom transform.
- [`./examples/use-cache`](./examples/use-cache) - Local cached functions and components demonstrating argument keys, Flight replay, dynamic children, captured values, and invalidation.
- [`./examples/use-cache-callable`](./examples/use-cache-callable) - Cached server references demonstrating inline and file directives, client invocation, argument admission, and encrypted closure captures.
- [`./examples/custom-server-function`](./examples/custom-server-function) - Third-party Server Function directive integration using server reference claims.
- [`./examples/ssg`](./examples/ssg) - Static site generation with MDX and client components for interactivity.
- [`./examples/ppr`](./examples/ppr) - Partial prerendering with a reusable static HTML shell and request-time RSC content.
Expand Down
105 changes: 105 additions & 0 deletions packages/plugin-rsc/examples/use-cache-callable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Callable `"use cache"` example

This example demonstrates a framework-owned `"use cache"` directive whose cached functions remain callable as React server references. It composes `@vitejs/plugin-rsc`'s generic directive transforms, server-reference registry, and low-level RSC serialization APIs.

Unlike the sibling [`use-cache`](../use-cache) example, these cached functions can cross a Server Component to Client Component boundary and run through hydrated or progressively enhanced forms. Neither React nor `@vitejs/plugin-rsc` defines `"use cache"`, and this example does not aim for full Next.js compatibility.

## Architecture

[`callable-cache-plugin.ts`](./callable-cache-plugin.ts) owns the directive policy by composing the public `transformWrapExport()`, `transformHoistInlineDirective()`, and `transformDirectiveProxyExport()` helpers. In the RSC environment it wraps module-level exports or hoists inline functions, registers the cached wrapper with React, and reports the reference through `getPluginApi().manager.serverReferences`. In browser and SSR environments it generates the corresponding server-reference proxies.

[`src/framework/use-cache-runtime.tsx`](./src/framework/use-cache-runtime.tsx) owns argument admission, cache identity, execution, and result replay:

```tsx
// -- input --
function Component() {
const captured = 'value'
async function cachedAction(argument: string) {
'use cache'
return `${captured}: ${argument}`
}
/// ...
}

// -- output ---
async function $$hoist_cachedAction(captures: unknown[], argument: string) {
const [captured] = captures
return `${captured}: ${argument}`
}

// callable externally as server function
export const $$hoist_reference_cachedAction = registerServerReference(
$$framework_cacheRuntime($$hoist_cachedAction, { argumentCount: 1 }),
)

function Component() {
const captured = 'value'
const cachedAction = $$reference.bind(
null,
$$framework_encryptCacheCaptures([captured]),
)
/// ...
}
```

Arguments are serialized with React's `encodeReply()` so values supported by the RSC protocol can participate in cache identity. On a miss, the runtime decodes those same arguments, invokes the private implementation, and stores its result as a replayable Flight stream.

## Examples

| Route | Demonstrates |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `/inline-directive` | An inline cached function captures a Server Component value and is passed to a Client Component form. |
| `/file-directive-from-server` | A module-level cached export is imported on the server and passed to a Client Component. |
| `/file-directive-from-client` | A Client Component imports a cached export through its generated proxy. |
| `/file-directive-extra-arguments` | A zero-parameter module export excludes React-supplied caller arguments. |
| `/inline-directive-extra-arguments` | A zero-parameter inline function uses transform metadata to exclude React-supplied caller arguments. |
| `/protected-captures` | Inline captures cross the client boundary encrypted while decoded values define cache identity. |

Each route displays submission and execution counts. Every form submission calls the server reference, while the function body runs only on a cache miss.

## Protected captures

Inline closure captures must not cross the client boundary as trusted plaintext. The transform's `encode` hook binds a framework-owned envelope with a synchronous sentinel and an asynchronous encrypted payload:

```text
registered wrapper.bind(null, encryptCacheCaptures([captured]))
```

The cache runtime recognizes the envelope, preserves it while admitting declared caller arguments, and decrypts it once. It then uses the same logical argument shape for cache identity and execution:

```ts
executionArguments = [captures, ...invocationArguments]
```

The transformed private implementation receives the decoded capture array and only destructures it back into source bindings. Decryption therefore belongs to the framework runtime rather than being repeated in generated output.

## Argument admission

The plugin derives `argumentCount` from function AST metadata. The runtime preserves an inline function's bound capture envelope, then admits only the declared invocation arguments. This prevents arguments supplied by helpers such as `useActionState()` from changing either cache identity or execution when the source function does not declare them. Functions with a rest parameter keep unrestricted argument admission.

## Form caveat

Hydrated forms rendered by SSR can retain React's `$ACTION_*` transport fields in their `FormData`. For an inline reference, those fields can include freshly encrypted bound captures, so submitting an unchanged direct form after a reload can miss the cache. This matches the behavior isolated by the [Next.js form reload reproduction](https://github.com/hi-ogawa/reproductions/tree/main/next-use-cache-form-reload).

Framework-specific form handling can avoid making React transport fields part of application cache identity. The protected-captures route demonstrates the small adapter approach by extracting its user field before calling the cached reference.

## Scope

The cache is process-local and in-memory. Entries are scoped to each wrapped function and can be reset or explicitly revalidated, but the example does not implement persistence, lifetimes, tags, eviction, distributed storage, or production invalidation policy. Its purpose is to demonstrate transform, server-reference transport, serialization, and protected-capture integration.

## Usage

```sh
pnpm dev
pnpm build
pnpm preview
```

## Source map

| Source | Responsibility |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| [`callable-cache-plugin.ts`](./callable-cache-plugin.ts) | Directive transforms, argument metadata, registration, and proxy generation. |
| [`src/framework/use-cache-runtime.tsx`](./src/framework/use-cache-runtime.tsx) | Capture adaptation, argument admission, cache keys, execution, and replay. |
| [`src/features`](./src/features) | Inline, file-level, caller-argument, and protected-capture scenarios. |
| [`../../e2e/use-cache-callable.test.ts`](../../e2e/use-cache-callable.test.ts) | Hydrated, progressive, development, and production behavioral coverage. |
71 changes: 68 additions & 3 deletions packages/plugin-rsc/examples/use-cache/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,74 @@
# `"use cache"` example
# Local `"use cache"` example

This example demonstrates a minimal cache feature inspired by Next.js's `"use cache"`. It composes `@vitejs/plugin-rsc`'s generic directive transform utilities with its low-level RSC runtime APIs.
This example demonstrates a minimal framework-owned cache for functions and components that execute inside the RSC environment. It composes `@vitejs/plugin-rsc`'s generic inline directive transform with its low-level RSC APIs.

Neither React nor `@vitejs/plugin-rsc` defines the `"use cache"` directive, and this example does not aim for full Next.js compatibility.
Unlike the sibling [`use-cache-callable`](../use-cache-callable) example, these cached functions remain local implementation details and do not become React server references callable from Client Components. Neither React nor `@vitejs/plugin-rsc` defines `"use cache"`, and this example does not aim for full Next.js compatibility.

## Composition

[`vite.config.ts`](./vite.config.ts) uses `transformHoistInlineDirective()` to move each async function containing `"use cache"` to module scope. Closure captures become leading arguments, and the transformed function is wrapped by [`src/framework/use-cache-runtime.tsx`](./src/framework/use-cache-runtime.tsx):

```js
// -- input --
function Component(prefix) {
async function cachedFn(value) {
'use cache'
return `${prefix}: ${value}`
}
/// ...
}

// -- output --
async function $$hoist_cachedFn(prefix, value) {
return `${prefix}: ${value}`
}

function Component(prefix) {
const cachedFn = $$framework_cacheRuntime($$hoist_cachedFn).bind(null, prefix)
/// ...
}
```

## Runtime semantics

The cache wrapper uses `encodeReply()` to serialize call arguments into the same protocol React uses for Server Function arguments and use it as a cache key. On a cache miss, the runtime reconstructs the arguments with `decodeReply()`, invokes the implementation, and serializes its result with `renderToReadableStream()`.

Results are retained as Flight streams. `StreamCacher` duplicates the stored stream for every read, and `createFromReadableStream()` decodes each branch with the temporary-reference set from that invocation. This preserves RSC serialization semantics when cached output contains React nodes or references.

## Examples

| Route | Demonstrates |
| ------------------- | ------------------------------------------------------------------------------------------------------------ |
| `/cached-function` | Equal arguments reuse an entry; `revalidateCache()` clears every entry associated with the wrapped function. |
| `/cached-component` | The component shell stays stable while temporary-reference `children` receive fresh values on each render. |
| `/captured-values` | Hoisted captured values and call-time arguments both participate in cache identity. |

The cached-function and captured-values routes display call and execution counts. Every submission calls the function, while the cached implementation runs only on a miss.

## Static shell and dynamic children

`CachedShell` receives its changing `children` inside a React element. With a temporary-reference set, `encodeReply()` records a reference marker rather than serializing that concrete element into the key. The cached Flight stream retains the corresponding placeholder, so replay can supply the current invocation's child while preserving the cached shell timestamp.

The wrapper element is intentional. A raw string child is serializable by value, so changing it would change the cache key and rerun the component instead of demonstrating a stable shell with a dynamic slot.

## Scope

The cache is process-local and in-memory. Entries are scoped to each wrapped function and support explicit invalidation, but the example does not implement persistence, lifetimes, tags, eviction, distributed storage, or production invalidation policy.

## Usage

```sh
pnpm dev
pnpm build
pnpm preview
```

## Source map

| Source | Responsibility |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| [`vite.config.ts`](./vite.config.ts) | Inline directive transform and runtime wrapping. |
| [`src/framework/use-cache-runtime.tsx`](./src/framework/use-cache-runtime.tsx) | Argument keys, miss execution, Flight stream storage, and replay. |
| [`src/root.tsx`](./src/root.tsx) | Scenario routing, navigation, and descriptions. |
| [`src/features`](./src/features) | Cached function, component shell, and captured-value scenarios. |
| [`../../e2e/use-cache.test.ts`](../../e2e/use-cache.test.ts) | Development and production behavioral coverage. |
Loading