-
Notifications
You must be signed in to change notification settings - Fork 46
feat: d1 adapter for the tag cache #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
00d2a0b
fix: enable using the `direct` queue for isr
james-elicx 5d77ec2
skip the ssr fetch cache test due to flakiness
james-elicx 4098e98
remove redundant import
james-elicx 0cd64bd
fix: enable using the `direct` queue for isr
james-elicx 2d1275b
skip the ssr fetch cache test due to flakiness
james-elicx c2eb151
feat: d1 adapter for the tag cache
james-elicx d6cae8f
re-use aws manifest output to create our manifest
james-elicx dd0816b
address review comments
james-elicx 5623685
use results instead of mapping over
james-elicx d13412b
output an sql file instead
james-elicx ea7e409
move file inside a cloudflare directory
james-elicx d63edc4
use a single insert statement
james-elicx 1feaaad
move the d1 setup to preview and e2e so you can still use skipbuild
james-elicx b0f1a19
use two tables for the tag cache
james-elicx 570722e
json.stringify
james-elicx e850841
move where the json.stringify is being done
james-elicx b28b327
insert unique tags only, and use recoverableerror
james-elicx 8034a33
re-use the manifest from the createcacheassets function
james-elicx 49fcd0f
re-use the useTagCache var from aws
james-elicx ebea5ed
fix flaky test
james-elicx 4b152b9
change type import location
james-elicx 4f8bddc
Revert "move where the json.stringify is being done"
james-elicx 631ed21
rename to tables
james-elicx 44f7332
add back comment that the revert wiped out
james-elicx 892667c
rebuild lockfile
james-elicx cfda346
Update packages/cloudflare/src/api/d1-tag-cache.ts
james-elicx 7b4730a
Update packages/cloudflare/src/api/d1-tag-cache.ts
james-elicx f5d8133
Update packages/cloudflare/src/api/d1-tag-cache.ts
james-elicx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@opennextjs/cloudflare": minor | ||
--- | ||
|
||
feat: d1 adapter for the tag cache |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import { debug, error } from "@opennextjs/aws/adapters/logger.js"; | ||
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js"; | ||
import type { TagCache } from "@opennextjs/aws/types/overrides.js"; | ||
import { RecoverableError } from "@opennextjs/aws/utils/error.js"; | ||
|
||
import { getCloudflareContext } from "./cloudflare-context.js"; | ||
|
||
/** | ||
* An instance of the Tag Cache that uses a D1 binding (`NEXT_CACHE_D1`) as it's underlying data store. | ||
* | ||
* **Tag/path mappings table** | ||
* | ||
* Information about the relation between tags and paths is stored in a `tags` table that contains | ||
* two columns; `tag`, and `path`. The table name can be configured with `NEXT_CACHE_D1_TAGS_TABLE` | ||
* environment variable. | ||
* | ||
* This table should be populated using an SQL file that is generated during the build process. | ||
* | ||
* **Tag revalidations table** | ||
* | ||
* Revalidation times for tags are stored in a `revalidations` table that contains two columns; `tags`, | ||
* and `revalidatedAt`. The table name can be configured with `NEXT_CACHE_D1_REVALIDATIONS_TABLE` | ||
* environment variable. | ||
*/ | ||
class D1TagCache implements TagCache { | ||
james-elicx marked this conversation as resolved.
Show resolved
Hide resolved
james-elicx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public readonly name = "d1-tag-cache"; | ||
|
||
public async getByPath(rawPath: string): Promise<string[]> { | ||
const { isDisabled, db, tables } = this.getConfig(); | ||
if (isDisabled) return []; | ||
|
||
const path = this.getCacheKey(rawPath); | ||
|
||
try { | ||
const { success, results } = await db | ||
.prepare(`SELECT tag FROM ${JSON.stringify(tables.tags)} WHERE path = ?`) | ||
.bind(path) | ||
.all<{ tag: string }>(); | ||
|
||
if (!success) throw new RecoverableError(`D1 select failed for ${path}`); | ||
|
||
const tags = results?.map((item) => this.removeBuildId(item.tag)); | ||
|
||
debug("tags for path", path, tags); | ||
return tags; | ||
} catch (e) { | ||
error("Failed to get tags by path", e); | ||
return []; | ||
} | ||
} | ||
|
||
public async getByTag(rawTag: string): Promise<string[]> { | ||
const { isDisabled, db, tables } = this.getConfig(); | ||
if (isDisabled) return []; | ||
|
||
const tag = this.getCacheKey(rawTag); | ||
|
||
try { | ||
const { success, results } = await db | ||
.prepare(`SELECT path FROM ${JSON.stringify(tables.tags)} WHERE tag = ?`) | ||
.bind(tag) | ||
.all<{ path: string }>(); | ||
|
||
if (!success) throw new RecoverableError(`D1 select failed for ${tag}`); | ||
|
||
const paths = results?.map((item) => this.removeBuildId(item.path)); | ||
|
||
debug("paths for tag", tag, paths); | ||
return paths; | ||
} catch (e) { | ||
error("Failed to get by tag", e); | ||
return []; | ||
} | ||
} | ||
|
||
public async getLastModified(path: string, lastModified?: number): Promise<number> { | ||
const { isDisabled, db, tables } = this.getConfig(); | ||
if (isDisabled) return lastModified ?? Date.now(); | ||
|
||
try { | ||
const { success, results } = await db | ||
.prepare( | ||
`SELECT ${JSON.stringify(tables.revalidations)}.tag FROM ${JSON.stringify(tables.revalidations)} | ||
INNER JOIN ${JSON.stringify(tables.tags)} ON ${JSON.stringify(tables.revalidations)}.tag = ${JSON.stringify(tables.tags)}.tag | ||
WHERE ${JSON.stringify(tables.tags)}.path = ? AND ${JSON.stringify(tables.revalidations)}.revalidatedAt > ?;` | ||
) | ||
.bind(this.getCacheKey(path), lastModified ?? 0) | ||
.all<{ tag: string }>(); | ||
|
||
if (!success) throw new RecoverableError(`D1 select failed for ${path} - ${lastModified ?? 0}`); | ||
|
||
debug("revalidatedTags", results); | ||
return results?.length > 0 ? -1 : (lastModified ?? Date.now()); | ||
} catch (e) { | ||
error("Failed to get revalidated tags", e); | ||
return lastModified ?? Date.now(); | ||
} | ||
} | ||
|
||
public async writeTags(tags: { tag: string; path: string; revalidatedAt?: number }[]): Promise<void> { | ||
const { isDisabled, db, tables } = this.getConfig(); | ||
if (isDisabled || tags.length === 0) return; | ||
|
||
try { | ||
const uniqueTags = new Set<string>(); | ||
const results = await db.batch( | ||
tags | ||
.map(({ tag, path, revalidatedAt }) => { | ||
if (revalidatedAt === 1) { | ||
// new tag/path mapping from set | ||
return db | ||
.prepare(`INSERT INTO ${JSON.stringify(tables.tags)} (tag, path) VALUES (?, ?)`) | ||
.bind(this.getCacheKey(tag), this.getCacheKey(path)); | ||
} | ||
|
||
if (!uniqueTags.has(tag) && revalidatedAt !== -1) { | ||
// tag was revalidated | ||
uniqueTags.add(tag); | ||
return db | ||
.prepare( | ||
`INSERT INTO ${JSON.stringify(tables.revalidations)} (tag, revalidatedAt) VALUES (?, ?)` | ||
) | ||
.bind(this.getCacheKey(tag), revalidatedAt ?? Date.now()); | ||
} | ||
}) | ||
.filter((stmt) => !!stmt) | ||
); | ||
|
||
const failedResults = results.filter((res) => !res.success); | ||
|
||
if (failedResults.length > 0) { | ||
throw new RecoverableError(`${failedResults.length} tags failed to write`); | ||
} | ||
} catch (e) { | ||
error("Failed to batch write tags", e); | ||
} | ||
} | ||
|
||
private getConfig() { | ||
const cfEnv = getCloudflareContext().env; | ||
const db = cfEnv.NEXT_CACHE_D1; | ||
|
||
if (!db) debug("No D1 database found"); | ||
|
||
const isDisabled = !!(globalThis as unknown as { openNextConfig: OpenNextConfig }).openNextConfig | ||
.dangerous?.disableTagCache; | ||
|
||
if (!db || isDisabled) { | ||
return { isDisabled: true as const }; | ||
} | ||
|
||
return { | ||
isDisabled: false as const, | ||
db, | ||
tables: { | ||
tags: cfEnv.NEXT_CACHE_D1_TAGS_TABLE ?? "tags", | ||
revalidations: cfEnv.NEXT_CACHE_D1_REVALIDATIONS_TABLE ?? "revalidations", | ||
}, | ||
}; | ||
} | ||
|
||
protected removeBuildId(key: string) { | ||
return key.replace(`${this.getBuildId()}/`, ""); | ||
} | ||
|
||
protected getCacheKey(key: string) { | ||
return `${this.getBuildId()}/${key}`.replaceAll("//", "/"); | ||
} | ||
|
||
protected getBuildId() { | ||
return process.env.NEXT_BUILD_ID ?? "no-build-id"; | ||
} | ||
} | ||
|
||
export default new D1TagCache(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
packages/cloudflare/src/cli/build/open-next/compile-cache-assets-manifest.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"; | ||
import path from "node:path"; | ||
|
||
import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; | ||
import type { TagCacheMetaFile } from "@opennextjs/aws/types/cache.js"; | ||
|
||
/** | ||
* Generates SQL statements that can be used to initialise the cache assets manifest in an SQL data store. | ||
*/ | ||
export function compileCacheAssetsManifestSqlFile(options: BuildOptions, metaFiles: TagCacheMetaFile[]) { | ||
const outputPath = path.join(options.outputDir, "cloudflare/cache-assets-manifest.sql"); | ||
|
||
const tagsTable = process.env.NEXT_CACHE_D1_TAGS_TABLE || "tags"; | ||
const revalidationsTable = process.env.NEXT_CACHE_D1_REVALIDATIONS_TABLE || "revalidations"; | ||
|
||
mkdirSync(path.dirname(outputPath), { recursive: true }); | ||
writeFileSync( | ||
outputPath, | ||
`CREATE TABLE IF NOT EXISTS ${JSON.stringify(tagsTable)} (tag TEXT NOT NULL, path TEXT NOT NULL, UNIQUE(tag, path) ON CONFLICT REPLACE); | ||
CREATE TABLE IF NOT EXISTS ${JSON.stringify(revalidationsTable)} (tag TEXT NOT NULL, revalidatedAt INTEGER NOT NULL, UNIQUE(tag) ON CONFLICT REPLACE);\n` | ||
); | ||
|
||
const values = metaFiles.map(({ tag, path }) => `(${JSON.stringify(tag.S)}, ${JSON.stringify(path.S)})`); | ||
|
||
if (values.length) { | ||
appendFileSync( | ||
outputPath, | ||
`INSERT INTO ${JSON.stringify(tagsTable)} (tag, path) VALUES ${values.join(", ")};` | ||
); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.