Skip to content
Merged
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
35 changes: 26 additions & 9 deletions packages/cache/src/fetchwithcache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { Buffer } from 'node:buffer'
import { Readable } from 'node:stream'
import type { ReadableStream } from 'node:stream/web'

import { describe, test, expect, beforeAll, afterAll } from 'vitest'
import { describe, test, expect, beforeEach, afterAll } from 'vitest'

import { NetlifyCacheStorage } from './bootstrap/cachestorage.js'
import type { FetchWithCache } from './fetchwithcache.js'
import { fetchWithCache } from './fetchwithcache.js'
import { getMockFetch } from './test/fetch.js'
import { readAsString, sleep } from './test/util.js'
import { decodeHeaders } from './test/headers.js'
Expand All @@ -17,17 +17,11 @@ const token = 'mock-token'

let originalCaches = globalThis.caches

let fetchWithCache: FetchWithCache

beforeAll(async () => {
beforeEach(async () => {
globalThis.caches = new NetlifyCacheStorage({
base64Encode,
getContext: () => ({ host, token, url }),
})

// Using a dynamic import so that `globalThis.caches` is populated by the
// time the polyfill is loaded.
fetchWithCache = (await import('./fetchwithcache.js')).fetchWithCache
})

afterAll(() => {
Expand Down Expand Up @@ -183,4 +177,27 @@ describe('`fetchWithCache`', () => {
expect(mockFetch.requests.length).toBe(4)
})
})

test('Uses the exported `caches` proxy', async () => {
const html = '<h1>Hello world</h1>'
const mockFetch = getMockFetch({
responses: {
'https://example.netlify/.netlify/cache/https%3A%2F%2Fnetlify.com%2F': [new Response(html)],
'https://netlify.com/': [new Response(html)],
},
})
const resourceURL = 'https://netlify.com'
const responseWithCache = await fetchWithCache(resourceURL)
expect(await responseWithCache.text()).toBe('<h1>Hello world</h1>')

// @ts-expect-error
delete globalThis.caches

const responseWithProxy = await fetchWithCache(resourceURL)
expect(await responseWithProxy.text()).toBe('<h1>Hello world</h1>')

mockFetch.restore()

expect(mockFetch.requests.length).toBe(2)
})
})
2 changes: 1 addition & 1 deletion packages/cache/src/fetchwithcache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const isRequestInit = (input: any): input is RequestInit => {
return false
}

export type FetchWithCache = {
type FetchWithCache = {
(request: string | URL | Request, init?: RequestInit): Promise<Response>
(request: string | URL | Request, cacheSettings?: CacheOptions): Promise<Response>
(request: string | URL | Request, init: RequestInit, cacheSettings?: CacheOptions): Promise<Response>
Expand Down
2 changes: 1 addition & 1 deletion packages/cache/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('`caches` export', () => {
getContext: () => ({ host, token, url }),
})

const cache2 = await globalThis.caches.open('cache2')
const cache2 = await caches.open('cache2')
const res2 = await cache2.match('https://netlify.com')
expect(res2?.status).toBe(200)
expect(await res2?.text()).toBe(html)
Expand Down
75 changes: 65 additions & 10 deletions packages/cache/src/polyfill.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,68 @@
import { NetlifyCacheStorage } from './bootstrap/cachestorage.js'

/**
* Polyfill for local development environments where `globalThis.caches` is not
* available. This is a no-op cache, which will automatically work with the
* real one in production.
* available. It's a no-op cache. In production it will use the real one that
* has been set up in the environment by the bootstrap layer.
*/
export const caches =
globalThis.caches ??
new NetlifyCacheStorage({
base64Encode: () => '',
getContext: () => null,
})
class NetlifyCacheStorageProxy implements CacheStorage {
async delete(name: string): Promise<boolean> {
if (globalThis.caches) {
return globalThis.caches.delete(name)
}

return false
}

async has(name: string): Promise<boolean> {
if (globalThis.caches) {
return globalThis.caches.has(name)
}

return false
}

async keys(): Promise<string[]> {
if (globalThis.caches) {
return globalThis.caches.keys()
}

return []
}

async match(request: RequestInfo, options?: MultiCacheQueryOptions): Promise<Response | undefined> {
if (globalThis.caches) {
return globalThis.caches.match(request, options)
}
}

async open(cacheName: string) {
if (globalThis.caches) {
return globalThis.caches.open(cacheName)
}

return new NetlifyNoopCache()
}
}

class NetlifyNoopCache implements Cache {
async add(_: RequestInfo): Promise<void> {}

async addAll(_: RequestInfo[]): Promise<void> {}

async delete(_: RequestInfo): Promise<boolean> {
return true
}

async keys(_?: Request): Promise<Array<Request>> {
return []
}

async match(_: RequestInfo): Promise<undefined> {}

async matchAll(_?: RequestInfo): Promise<readonly Response[]> {
return []
}

async put(_: RequestInfo | URL | string, __: Response) {}
}

export const caches = new NetlifyCacheStorageProxy()
Loading