Skip to content
Open
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
14 changes: 9 additions & 5 deletions modules/snapshot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ const logger = useLogger('comark-docs')
const ASSET_BASE = 'comark-content'

/**
* Writes a build-time content snapshot into the function bundle.
* A cold start then hydrates from it instead of walking the content repository.
* Writes a build-time content snapshot into the function bundle, stamped with the commit it was
* parsed at. A cold start at that commit hydrates from it instead of walking the content
* repository; a cold start at a later commit reuses every unchanged body from it.
*/
export default defineNuxtModule({
meta: { name: 'comark-docs:snapshot' },
Expand Down Expand Up @@ -52,16 +53,19 @@ export default defineNuxtModule({
return
}

const content = createBuildContentInstance({ source: fs(contentPath) })
// Pinned to the content commit, so the artifact carries `ref`. At runtime an instance pinned
// to the same commit uses it as its index; one pinned to a later commit walks that commit
// for the index and still takes every body whose source text did not change.
const content = createBuildContentInstance({ source: fs(contentPath) }).withRef(sha)

try {
const writeStart = performance.now()
await writeSnapshots(content, { dir: join(dir, sha), manifest: false })
await writeSnapshots(content, { dir, manifest: false })
const writeMs = Math.round(performance.now() - writeStart)

// Size is the number to watch: the snapshot is inlined into the bundle as a string.
// Every cold start that reads it pays for that.
const { size } = await stat(join(dir, sha, DEFAULT_CONTENT_NAME, 'snapshot.json'))
const { size } = await stat(join(dir, DEFAULT_CONTENT_NAME, 'snapshot.json'))
logger.success(
`Content snapshot ${sha.slice(0, 7)}: ${Math.round(size / 1024)} kB parsed and written in ${writeMs}ms ` +
`(ref resolved in ${resolveMs}ms)`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"ai": "^7.0.77",
"beautiful-mermaid": "^1.1.3",
"comark": "^0.6.2",
"comark-content": "https://pkg.pr.new/comark-content@63ffc3f",
"comark-content": "https://pkg.pr.new/comark-content@98457f7",
"defu": "^6.1.7",
"exsolve": "^1.1.1",
"js-yaml": "^5.3.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 2 additions & 16 deletions server/api/content/blob/[sha]/[...path].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,6 @@ export default defineEventHandler(async (event) => {
// Also resolves short SHAs so one commit pins one content instance.
const fullSha = await authorizePreviewSha(sha)

// Head-of-branch requests reuse the shared prod instance (same source ref, same per-SHA cache
// namespace) instead of minting a duplicate preview instance that would pin an LRU slot with a
// clone of production. Re-checked after `getProdContent()`, which may advance the head.
if (fullSha === getHeadRef()) {
const prod = await getProdContent()
if (fullSha === getHeadRef()) {
const request = toWebRequest(event)
const url = new URL(request.url)
url.pathname = url.pathname.replace(`/blob/${rawSha}`, '')
return await prod.handler(new Request(url, request))
}
}

const content = await getPreviewContent(fullSha, `/api/content/blob/${sha}`)

return await content.handler(toWebRequest(event))
// Head-of-branch requests reuse the shared prod instance; `servePreview()` handles that.
return servePreview(event, fullSha, `/blob/${rawSha}`)
})
4 changes: 1 addition & 3 deletions server/api/content/pr/[number]/[...path].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,5 @@ export default defineEventHandler(async (event) => {
}

const sha = await resolvePullPreviewSha(number)
const content = await getPreviewContent(sha, `/api/content/pr/${number}`)

return await content.handler(toWebRequest(event))
return servePreview(event, sha, `/pr/${rawNumber}`)
})
4 changes: 1 addition & 3 deletions server/api/content/tree/[branch]/[...path].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,5 @@ export default defineEventHandler(async (event) => {

// `cacheMisses`: the ref comes from the URL, so a miss must not re-cost a GitHub call each time.
const sha = await resolveContentSha(branch, useRuntimeConfig(event).docs.contentDir, { cacheMisses: true })
const content = await getPreviewContent(sha, `/api/content/tree/${encodeURIComponent(branch)}`)

return await content.handler(toWebRequest(event))
return servePreview(event, sha, `/tree/${rawBranch}`)
})
10 changes: 6 additions & 4 deletions server/api/revalidate.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ export default defineEventHandler(async (event) => {

// Refresh the content SHA
const headSha = await resolveContentSha(branch, contentDir, { refresh: true })
const freshContent = await createSourceContent(headSha, { cache: { driver: cacheDriver(headSha) } })
// Partial init: the diff needs the index (cache will be reused by the warm below)
await freshContent.init()
const newItems = (await freshContent.manifest()).items
// A throwaway instance pinned to the new commit: the diff needs its index only. Its index lands
// in the commit's cache namespace, which the prod swap and the warm below then reuse.
const fresh = contentAt(headSha)
await fresh.init()
const newItems = (await fresh.manifest()).items
await fresh.dispose()

return { headSha, newItems, ...diffContent(changes, oldItems, newItems) }
})
Expand Down
24 changes: 15 additions & 9 deletions server/utils/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,28 @@ function cacheAvailable(): boolean {
*/
export const CONTENT_PARSER_VERSION = 'v3'

/** Per-parser-version, per-content-SHA driver backing comark's manifest and parsed bodies. */
export function cacheDriver(sha: string): Driver {
/**
* The driver behind everything cached per parser version, under `content:<version>`:
*
* - without `sha`, comark's index, parsed bodies and artifacts for every commit. An instance pinned
* with `content.withRef(sha)` adds its own `ref:<sha>:` prefix, so instances pinned to different
* commits share this driver without reading each other's entries;
* - with `sha`, ad-hoc per-commit data (commit history, RSS dates) under `content:<version>:<sha>`.
* Those keys start with `gh:` and never meet comark's.
*
* Bumping the parser version leaves every commit's entries behind at once.
*/
export function contentCacheDriver(sha?: string): Driver {
if (!cacheAvailable()) return memoryDriver()
return vercelRuntimeCache({
base: `content:${CONTENT_PARSER_VERSION}:${sha}`,
base: sha ? `content:${CONTENT_PARSER_VERSION}:${sha}` : `content:${CONTENT_PARSER_VERSION}`,
ttl: TTL,
})
}

/**
* Ad-hoc per-SHA storage for non-content data (commit history, RSS dates). Shares comark's
* `cacheDriver(sha)` namespace rather than a separate unconfigured mount; `gh:...` keys can't
* collide with comark's `<source>:<path>`.
*/
/** Ad-hoc per-SHA storage for non-content data (commit history, RSS dates). */
export function shaCacheStorage(sha: string): Storage {
return createStorage({ driver: cacheDriver(sha) })
return createStorage({ driver: contentCacheDriver(sha) })
}

/**
Expand Down
Loading