|
| 1 | +// Netlify Cache Handler |
| 2 | +// (CJS format because Next.js doesn't support ESM yet) |
| 3 | +// |
1 | 4 | import { getDeployStore } from '@netlify/blobs'
|
2 | 5 | import { purgeCache } from '@netlify/functions'
|
3 | 6 | import type {
|
4 | 7 | CacheHandler,
|
5 | 8 | CacheHandlerContext,
|
| 9 | + IncrementalCache, |
6 | 10 | } from 'next/dist/server/lib/incremental-cache/index.js'
|
| 11 | +import { join } from 'node:path/posix' |
| 12 | +// @ts-expect-error This is a type only import |
| 13 | +import type { CacheEntryValue } from '../../build/content/prerendered.js' |
7 | 14 |
|
8 | 15 | type TagManifest = { revalidatedAt: number }
|
9 | 16 |
|
10 | 17 | const tagsManifestPath = '_netlify-cache/tags'
|
11 | 18 | const blobStore = getDeployStore()
|
12 | 19 |
|
13 |
| -/** |
14 |
| - * Netlify Cache Handler |
15 |
| - * (CJS format because Next.js doesn't support ESM yet) |
16 |
| - */ |
17 | 20 | export default class NetlifyCacheHandler implements CacheHandler {
|
18 | 21 | options: CacheHandlerContext
|
19 | 22 | revalidatedTags: string[]
|
| 23 | + /** Indicates if the application is using the new appDir */ |
| 24 | + #appDir: boolean |
20 | 25 |
|
21 | 26 | constructor(options: CacheHandlerContext) {
|
| 27 | + this.#appDir = Boolean(options._appDir) |
22 | 28 | this.options = options
|
23 | 29 | this.revalidatedTags = options.revalidatedTags
|
24 | 30 | }
|
25 | 31 |
|
26 |
| - // eslint-disable-next-line require-await, class-methods-use-this |
27 |
| - public async get(key: string, ctx: any) { |
28 |
| - console.log('NetlifyCacheHandler.get', key, JSON.stringify(ctx, null, 2)) |
| 32 | + async get(...args: Parameters<CacheHandler['get']>): ReturnType<CacheHandler['get']> { |
| 33 | + const [cacheKey, ctx = {}] = args |
| 34 | + console.log(`[NetlifyCacheHandler.get]: ${cacheKey}`) |
| 35 | + const blob = await this.getBlobKey(cacheKey, ctx.fetchCache) |
| 36 | + |
| 37 | + switch (blob?.value?.kind) { |
| 38 | + // TODO: |
| 39 | + // case 'ROUTE': |
| 40 | + // case 'FETCH': |
| 41 | + case 'PAGE': |
| 42 | + // TODO: determine if the page is stale based on the blob.lastModified Date.now() |
| 43 | + return { |
| 44 | + lastModified: blob.lastModified, |
| 45 | + value: { |
| 46 | + kind: 'PAGE', |
| 47 | + html: blob.value.html, |
| 48 | + pageData: blob.value.pageData, |
| 49 | + headers: blob.value.headers, |
| 50 | + status: blob.value.status, |
| 51 | + }, |
| 52 | + } |
| 53 | + |
| 54 | + default: |
| 55 | + console.log('TODO: implmenet', blob) |
| 56 | + } |
29 | 57 | return null
|
30 | 58 | }
|
31 | 59 |
|
32 | 60 | // eslint-disable-next-line require-await, class-methods-use-this
|
33 |
| - public async set(key: string, data: any, ctx: any) { |
34 |
| - console.log( |
35 |
| - 'NetlifyCacheHandler.set', |
36 |
| - key, |
37 |
| - JSON.stringify(data, null, 2), |
38 |
| - JSON.stringify(ctx, null, 2), |
39 |
| - ) |
| 61 | + async set(...args: Parameters<IncrementalCache['set']>) { |
| 62 | + const [key, data, ctx] = args |
| 63 | + console.log('NetlifyCacheHandler.set', key) |
40 | 64 | }
|
41 | 65 |
|
42 |
| - // eslint-disable-next-line require-await |
43 |
| - public async revalidateTag(tag: string) { |
| 66 | + async revalidateTag(tag: string) { |
44 | 67 | console.log('NetlifyCacheHandler.revalidateTag', tag)
|
45 | 68 |
|
46 | 69 | const data: TagManifest = {
|
47 | 70 | revalidatedAt: Date.now(),
|
48 | 71 | }
|
49 | 72 |
|
50 | 73 | try {
|
51 |
| - blobStore.setJSON(this.tagManifestPath(tag), data) |
52 |
| - } catch (error: any) { |
| 74 | + await blobStore.setJSON(this.tagManifestPath(tag), data) |
| 75 | + } catch (error) { |
53 | 76 | console.warn(`Failed to update tag manifest for ${tag}`, error)
|
54 | 77 | }
|
55 | 78 |
|
56 | 79 | purgeCache({ tags: [tag] })
|
57 | 80 | }
|
58 | 81 |
|
59 |
| - private async loadTagManifest(tag: string) { |
60 |
| - try { |
61 |
| - return await blobStore.get(this.tagManifestPath(tag), { type: 'json' }) |
62 |
| - } catch (error: any) { |
63 |
| - console.warn(`Failed to fetch tag manifest for ${tag}`, error) |
64 |
| - } |
65 |
| - } |
66 |
| - |
67 | 82 | // eslint-disable-next-line class-methods-use-this
|
68 | 83 | private tagManifestPath(tag: string) {
|
69 | 84 | return [tagsManifestPath, tag].join('/')
|
70 | 85 | }
|
| 86 | + |
| 87 | + /** |
| 88 | + * Computes a cache key and tries to load it for different scenarios (app/server or fetch) |
| 89 | + * @param key The cache key used by next.js |
| 90 | + * @param fetch If it is a FETCH request or not |
| 91 | + * @returns the parsed data from the cache or null if not |
| 92 | + */ |
| 93 | + private async getBlobKey( |
| 94 | + key: string, |
| 95 | + fetch?: boolean, |
| 96 | + ): Promise< |
| 97 | + | null |
| 98 | + | ({ |
| 99 | + path: string |
| 100 | + isAppPath: boolean |
| 101 | + } & CacheEntryValue) |
| 102 | + > { |
| 103 | + const appKey = join('server/app', key) |
| 104 | + const pagesKey = join('server/pages', key) |
| 105 | + const fetchKey = join('cache/fetch-cache', key) |
| 106 | + |
| 107 | + if (fetch) { |
| 108 | + return await blobStore |
| 109 | + .get(fetchKey, { type: 'json' }) |
| 110 | + .then((res) => (res !== null ? { path: fetchKey, isAppPath: false, ...res } : null)) |
| 111 | + } |
| 112 | + |
| 113 | + // pagesKey needs to be requested first as there could be both sadly |
| 114 | + const values = await Promise.all([ |
| 115 | + blobStore |
| 116 | + .get(pagesKey, { type: 'json' }) |
| 117 | + .then((res) => ({ path: pagesKey, isAppPath: false, ...res })), |
| 118 | + // only request the appKey if the whole application supports the app key |
| 119 | + !this.#appDir |
| 120 | + ? Promise.resolve(null) |
| 121 | + : blobStore |
| 122 | + .get(appKey, { type: 'json' }) |
| 123 | + .then((res) => ({ path: appKey, isAppPath: true, ...res })), |
| 124 | + ]) |
| 125 | + |
| 126 | + // just get the first item out of it that is defined (either the pageRoute or the appRoute) |
| 127 | + const [cacheEntry] = values.filter(({ value }) => !!value) |
| 128 | + |
| 129 | + // TODO: set the cache tags based on the tag manifest once we have that |
| 130 | + // if (cacheEntry) { |
| 131 | + // const cacheTags: string[] = |
| 132 | + // cacheEntry.value.headers?.[NEXT_CACHE_TAGS_HEADER]?.split(',') || [] |
| 133 | + // const manifests = await Promise.all( |
| 134 | + // cacheTags.map((tag) => blobStore.get(this.tagManifestPath(tag), { type: 'json' })), |
| 135 | + // ) |
| 136 | + // console.log(manifests) |
| 137 | + // } |
| 138 | + |
| 139 | + return cacheEntry || null |
| 140 | + } |
71 | 141 | }
|
0 commit comments