Skip to content

Port SWR PR's#30

Open
conico974 wants to merge 6 commits intomainfrom
conico/port-swr
Open

Port SWR PR's#30
conico974 wants to merge 6 commits intomainfrom
conico/port-swr

Conversation

@conico974
Copy link
Copy Markdown
Contributor

@conico974 conico974 commented May 5, 2026

Ported SWR PR's.
I haven't finished reviewing the port yet


Open in Devin Review

conico974 added 6 commits May 2, 2026 18:10
…h revalidateTag

Ported from opennextjs/opennextjs-aws#1122

Adds stale-while-revalidate (SWR) support for revalidateTag in Next.js 16+,
including isStale methods on tag caches, a per-request RequestCache utility,
runtime nextVersion for version checks, updateTags on composable cache handler,
and the provideInternalWaitUntil patch.

Changeset: .changeset/port-pr-1122.md
Add support for SWR (stale-while-revalidate) in revalidateTag

opennextjs/opennextjs-cloudflare#1168

Changeset: .changeset/port-pr-1168.md
Copilot AI review requested due to automatic review settings May 5, 2026 22:00
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 5, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@opennextjs/cloudflare@30
npm i https://pkg.pr.new/@opennextjs/aws@30

commit: 7cc3b0c

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +170 to +177
const compute = (item: any): boolean => {
if (!item) return false;
if (item.expire?.N) {
const expiry = Number.parseInt(item.expire.N);
if (expiry <= now && expiry > (lastModified ?? 0)) return true;
}
return Number.parseInt(item.revalidatedAt.N) > (lastModified ?? 0);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 dynamodb-nextMode hasBeenRevalidated falls through to revalidatedAt check for SWR-window tags, defeating stale-while-revalidate

In dynamodb-nextMode.ts, the compute function inside hasBeenRevalidated does not return false when a tag has expire set but not yet expired (SWR window). The if (item.expire?.N) { if (expiry <= now ...) return true; } block only handles the expired case — when expire > now (SWR window), it falls through to return Number.parseInt(item.revalidatedAt.N) > (lastModified ?? 0), which returns true, fully invalidating the cache entry.

All the Cloudflare implementations (d1-next-tag-cache.ts:49, kv-next-tag-cache.ts:114, do-sharded-tag-cache.ts:166) use if (expire != null) return ...; return revalidatedAt > ...; — when expire is set, they always return based on the expire check only. This ensures SWR-window tags return false from hasBeenRevalidated, allowing isStale() to handle them. The DynamoDB implementation misses this pattern, so the SWR feature is effectively broken for this tag cache backend.

Suggested change
const compute = (item: any): boolean => {
if (!item) return false;
if (item.expire?.N) {
const expiry = Number.parseInt(item.expire.N);
if (expiry <= now && expiry > (lastModified ?? 0)) return true;
}
return Number.parseInt(item.revalidatedAt.N) > (lastModified ?? 0);
};
const compute = (item: any): boolean => {
if (!item) return false;
if (item.expire?.N) {
const expiry = Number.parseInt(item.expire.N);
return expiry <= now && expiry > (lastModified ?? 0);
}
return Number.parseInt(item.revalidatedAt.N) > (lastModified ?? 0);
};
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +162 to +167
const nonExpiredRevalidatedTags = revalidatedTags.filter((item) => {
if (item.expire?.N) {
return Number.parseInt(item.expire.N) > now;
}
return true;
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 dynamodb (full SDK) getLastModified includes SWR-window tags in nonExpiredRevalidatedTags, causing full invalidation during SWR window

In dynamodb.ts, the nonExpiredRevalidatedTags filter keeps tags where expire > now (i.e., tags still in the SWR window). Since nonExpiredRevalidatedTags.length > 0 causes getLastModified to return -1, any cache entry associated with an SWR-revalidated tag is fully invalidated during the SWR window instead of being served stale.

The correct behavior (matching dynamodb-lite.ts:202-204) is to exclude all tags that have an expire field set but haven't fully expired yet, since expired tags are already handled by hasExpiredTag, and SWR-window tags should be handled by isStale() instead.

Comparison with dynamodb-lite.ts

The dynamodb-lite.ts version filters differently:

return Number.parseInt(item.expire.N) === Number.parseInt(item.revalidatedAt.N);

This effectively excludes SWR-window tags (where expire ≠ revalidatedAt), though this approach is fragile. A cleaner fix is to exclude all tags with an expire field from the non-expired count.

Suggested change
const nonExpiredRevalidatedTags = revalidatedTags.filter((item) => {
if (item.expire?.N) {
return Number.parseInt(item.expire.N) > now;
}
return true;
});
const nonExpiredRevalidatedTags = revalidatedTags.filter((item) => {
if (item.expire?.N) {
// Tags with expire are either:
// - expired (expire <= now): already handled by hasExpiredTag
// - in SWR window (expire > now): handled by isStale()
return false;
}
return true;
});
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ports multiple upstream SWR-related changes, adding stale-while-revalidate support to tag-based revalidation across the AWS and Cloudflare runtimes, along with per-request caching to reduce repeated tag-cache lookups and new tests to validate the behavior.

Changes:

  • Introduces SWR-aware tag invalidation metadata (stale / expire) and an isStale() path across TagCache implementations.
  • Adds per-request in-memory caching (RequestCache) to deduplicate tag-cache reads within a request.
  • Updates cache adapters/interceptor and patches to support Next 16+ behavior (including internal waitUntil) and expands unit tests.

Reviewed changes

Copilot reviewed 44 out of 45 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
pnpm-lock.yaml Updates lockfile to include new AWS SDK dependency resolution.
packages/tests-unit/tests/overrides/tagCache/dynamodb.test.ts Adds unit tests for DynamoDB (original mode) tag cache behavior/caching.
packages/tests-unit/tests/overrides/tagCache/dynamodb-nextMode.test.ts Adds unit tests for DynamoDB nextMode tag cache SWR behavior.
packages/tests-unit/tests/overrides/tagCache/dynamodb-lite.test.ts Adds unit tests for DynamoDB-lite tag cache SWR behavior/caching.
packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts Adds tests for stale serving behavior via tagCache.isStale.
packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts Tests new NextServer patch for internal waitUntil.
packages/tests-unit/tests/adapters/composable-cache.test.ts Adds tests for stale behavior + updateTags in composable cache.
packages/tests-unit/tests/adapters/cache.test.ts Updates cache adapter tests for SWR tag behavior and API changes.
packages/tests-unit/package.json Adds @aws-sdk/client-dynamodb dev dependency for unit tests.
packages/open-next/src/utils/semver.ts Adds shared semver comparison helper.
packages/open-next/src/utils/requestCache.ts Introduces per-request RequestCache.
packages/open-next/src/utils/promise.ts Adds requestCache to the request ALS context.
packages/open-next/src/utils/cache.ts Adds isStale() helper + extends tag write dedupe keys for new inputs.
packages/open-next/src/types/overrides.ts Extends TagCache types for SWR (stale/expire) + isStale.
packages/open-next/src/types/global.ts Adds requestCache to ALS context and declares nextVersion.
packages/open-next/src/types/cache.ts Extends tag-cache meta types and composable cache handler API (updateTags).
packages/open-next/src/overrides/tagCache/fs-dev.ts Adds SWR fields + isStale to fs-dev original tag cache.
packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts Adds SWR fields + isStale to fs-dev nextMode tag cache.
packages/open-next/src/overrides/tagCache/dynamodb.ts Adds SWR fields + requestCache usage + isStale to DynamoDB tag cache.
packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts Adds SWR fields + requestCache usage + isStale for DynamoDB nextMode.
packages/open-next/src/overrides/tagCache/dynamodb-lite.ts Adds SWR fields + requestCache usage + isStale for DynamoDB-lite.
packages/open-next/src/overrides/tagCache/dummy.ts Adds isStale stub to dummy tag cache.
packages/open-next/src/core/routing/cacheInterceptor.ts Uses isStale() to serve stale and trigger background revalidation.
packages/open-next/src/build/patch/patches/patchNextServer.ts Adds patch rule to provide internal waitUntil for Next 16+.
packages/open-next/src/build/helper.ts Re-exports semver helper from new shared module.
packages/open-next/src/build/compileCache.ts Injects globalThis.nextVersion into cache bundles.
packages/open-next/src/adapters/dynamo-provider.ts Supports parsing/passing SWR fields when inserting into tag cache.
packages/open-next/src/adapters/composable-cache.ts Adds staleness handling + implements updateTags.
packages/open-next/src/adapters/cache.ts Adds staleness handling + extends revalidateTag with durations.
packages/cloudflare/src/cli/commands/populate-cache.ts Updates D1 schema creation/migration for SWR fields.
packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts Updates tag filtering to support new write input shape + isStale.
packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.ts Adds SWR storage format + requestCache usage + isStale for KV.
packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.spec.ts Adds/updates KV nextMode SWR tests.
packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts Adds SWR TagData + requestCache usage + isStale for sharded DO cache.
packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.spec.ts Updates sharded DO tag cache tests for new SWR logic and APIs.
packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.ts Adds SWR fields + requestCache usage + isStale for D1 tag cache.
packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.spec.ts Adds/updates D1 nextMode SWR tests, incl. requestCache coverage.
packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts Adjusts defaults/notes for bypass behavior with Next 16+ SWR.
packages/cloudflare/src/api/durable-objects/sharded-tag-cache.ts Adds SWR fields + schema migration + new TagData API for DO.
packages/cloudflare/src/api/durable-objects/sharded-tag-cache.spec.ts Adds/updates DO sharded tag cache tests for SWR fields.
.changeset/port-pr-1200.md Changeset for Cloudflare package port.
.changeset/port-pr-1193.md Changeset for Cloudflare package port + stale logic fixes.
.changeset/port-pr-1168.md Changeset for Cloudflare package SWR support.
.changeset/port-pr-1142.md Changeset for AWS package cache stale logic enhancements.
.changeset/port-pr-1122.md Changeset for AWS package SWR support.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +22 to +25
export async function isStale(key: string, tags: string[], lastModified?: number): Promise<boolean> {
// SWR for revalidateTag has been implemented starting from Next.js 16
if (!compareSemver(globalThis.nextVersion, ">=", "16.0.0")) {
return false;
if (!item) return false;
if (item.expire?.N) {
const expiry = Number.parseInt(item.expire.N);
if (expiry <= now && expiry > (lastModified ?? 0)) return true;
Comment on lines +162 to +166
const nonExpiredRevalidatedTags = revalidatedTags.filter((item) => {
if (item.expire?.N) {
return Number.parseInt(item.expire.N) > now;
}
return true;
Comment on lines +206 to +210
itemsCache?.set(cacheKey, items);
}
debug("isStale items", key, items);
return items.length > 0;
} catch (e) {
Comment on lines +252 to +257
items = ((await result.json()) as any).Items ?? [];
itemsCache?.set(cacheKey, items);
}
debug("isStale items", key, items);
return items.length > 0;
} catch (e) {
Comment on lines +49 to +56
const nonExpiredRevalidatedTags = tags.filter(
(tagPathMapping) =>
tagPathMapping.path.S === buildKey(path) &&
Number.parseInt(tagPathMapping.revalidatedAt.N) > (lastModified ?? 0)
Number.parseInt(tagPathMapping.revalidatedAt.N) > (lastModified ?? 0) &&
(!tagPathMapping.expire?.N || Number.parseInt(tagPathMapping.expire.N) > now)
);
return revalidatedTags.length > 0 ? -1 : (lastModified ?? Date.now());

return nonExpiredRevalidatedTags.length > 0 || hasExpiredTag ? -1 : (lastModified ?? Date.now());
Comment on lines +44 to +50
const result = await this.#resolveTagValues(tags, db);

const revalidated = [...result.values()].some((v) => {
if (v == null) return false;
const { revalidatedAt, expire } = v;
if (expire != null) return expire <= now && expire > (lastModified ?? 0);
return revalidatedAt > (lastModified ?? now);
Comment on lines +113 to +115
const expire = getExpire(v);
if (expire != null) return expire <= now && expire > (lastModified ?? 0);
return getRevalidatedAt(v) > (lastModified ?? now);
async hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean> {
const data = await this.getTagData(tags);
const lastModifiedOrNowMs = lastModified ?? Date.now();
const revalidated = Object.values(data).some(({ revalidatedAt }) => revalidatedAt > lastModifiedOrNowMs);
Comment on lines +184 to +186
* - **Stale**: at least one tag was revalidated after `lastModified` but has not yet expired →
* returns `true`. The cache entry is still served but `revalidate` is set to `1` to trigger
* background revalidation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants