Skip to content
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

feat: further compatibility with Cloudflare Workers Cache API #33

Merged
merged 1 commit into from
Jul 9, 2024
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
6 changes: 6 additions & 0 deletions .changeset/khaki-dots-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@web-widget/shared-cache': patch
---

- The stale-while-revalidate and stale-if-error directives are not supported when using the cache.put or cache.match methods.
- Support HEAD requests.
48 changes: 1 addition & 47 deletions src/cache-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,12 @@ test('should support built-in rules', async () => {
include: ['x-id'],
},
host: true,
method: true,
pathname: true,
search: true,
},
}
);
expect(key).toBe('localhost/?a=1#a=356a19:desktop:x-id=a9993e:GET');
expect(key).toBe('localhost/?a=1#a=356a19:desktop:x-id=a9993e');
});

test('should support filtering', async () => {
Expand Down Expand Up @@ -410,51 +409,6 @@ describe('should support host', () => {
});
});

describe('should support method', () => {
test('basic', async () => {
const keyGenerator = createCacheKeyGenerator();
const key = await keyGenerator(new Request('http://localhost/'), {
cacheKeyRules: {
method: true,
},
});
expect(key).toBe('#GET');
});

test('should support filtering', async () => {
const keyGenerator = createCacheKeyGenerator();
const key = await keyGenerator(
new Request('http://localhost/', { method: 'POST' }),
{
cacheKeyRules: {
method: { include: ['GET'] },
},
}
);
expect(key).toBe('');
});

test('the body of the POST, PATCH and PUT methods should be used as part of the key', async () => {
await Promise.all(
['POST', 'PATCH', 'PUT'].map(async (method) => {
const keyGenerator = createCacheKeyGenerator();
const key = await keyGenerator(
new Request('http://localhost/', {
method,
body: 'hello',
}),
{
cacheKeyRules: {
method: true,
},
}
);
expect(key).toBe(`#${method}=aaf4c6`);
})
);
});
});

describe('should support pathname', () => {
test('basic', async () => {
const keyGenerator = createCacheKeyGenerator();
Expand Down
22 changes: 1 addition & 21 deletions src/cache-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export interface SharedCacheKeyRules {
header?: FilterOptions | boolean;
/** Use host as part of cache key. */
host?: FilterOptions | boolean;
/** Use method as part of cache key. */
method?: FilterOptions | boolean;
/** Use pathname as part of cache key. */
pathname?: FilterOptions | boolean;
/** Use search as part of cache key. */
Expand Down Expand Up @@ -121,18 +119,6 @@ export function host(url: URL, options?: FilterOptions) {
.join('');
}

export async function method(request: Request, options?: FilterOptions) {
const hasBody =
request.body && ['POST', 'PATCH', 'PUT'].includes(request.method);
return (
await Promise.all(
filter([[request.method, '']], options).map(async ([key]) =>
hasBody ? `${key}=${await shortHash(request.body)}` : key
)
)
).join('');
}

export function pathname(url: URL, options?: FilterOptions) {
const pathname = url.pathname;
return filter([[pathname, '']], options)
Expand Down Expand Up @@ -218,12 +204,10 @@ const BUILT_IN_EXPANDED_PART_DEFINERS: BuiltInExpandedCacheKeyPartDefiners = {
cookie,
device,
header,
method,
};

export const DEFAULT_CACHE_KEY_RULES: SharedCacheKeyRules = {
host: true,
method: true,
pathname: true,
search: true,
};
Expand Down Expand Up @@ -251,10 +235,6 @@ export function createCacheKeyGenerator(
const urlRules: SharedCacheKeyRules = { host, pathname, search };
const url = new URL(request.url);

if (options.ignoreMethod) {
fragmentRules.method = false;
}

const urlPart: string[] = BUILT_IN_URL_PART_KEYS.filter(
(name) => urlRules[name]
).map((name) => {
Expand Down Expand Up @@ -306,6 +286,6 @@ export function createCacheKeyGenerator(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function notImplemented(options: any, name: string) {
if (name in options) {
throw new Error(`Not Implemented: "${name}" option.`);
throw new Error(`Not implemented: "${name}" option.`);
}
}
8 changes: 4 additions & 4 deletions src/cache-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@ export class SharedCacheStorage implements CacheStorage {

/** @private */
async delete(_cacheName: string): Promise<boolean> {
throw new Error('Not Implemented.');
throw new Error('Not implemented.');
}

/** @private */
async has(_cacheName: string): Promise<boolean> {
throw new Error('Not Implemented.');
throw new Error('Not implemented.');
}

/** @private */
async keys(): Promise<string[]> {
throw new Error('Not Implemented.');
throw new Error('Not implemented.');
}

/** @private */
async match(
_request: RequestInfo,
_options?: MultiCacheQueryOptions
): Promise<Response | undefined> {
throw new Error('Not Implemented.');
throw new Error('Not implemented.');
}

/**
Expand Down
46 changes: 26 additions & 20 deletions src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ import {
STALE,
} from './constants';

const ORIGINAL_FETCH = globalThis.fetch;

export class SharedCache implements Cache {
#cacheKeyGenerator: (
request: Request,
options?: SharedCacheQueryOptions
) => Promise<string>;
#cacheKeyRules?: SharedCacheKeyRules;
#fetch: typeof fetch;
#fetch?: typeof fetch;
#logger?: Logger;
#storage: KVStorage;
#waitUntil: (promise: Promise<unknown>) => void;
Expand All @@ -48,20 +46,20 @@ export class SharedCache implements Cache {
resolveOptions.cacheKeyPartDefiners
);
this.#cacheKeyRules = resolveOptions.cacheKeyRules;
this.#fetch = resolveOptions.fetch ?? ORIGINAL_FETCH;
this.#fetch = resolveOptions.fetch;
this.#logger = resolveOptions.logger;
this.#storage = storage;
this.#waitUntil = resolveOptions.waitUntil;
}

/** @private */
async add(_request: RequestInfo): Promise<void> {
throw new Error('Not Implemented.');
throw new Error('Not implemented.');
}

/** @private */
async addAll(_requests: RequestInfo[]): Promise<void> {
throw new Error('Not Implemented.');
throw new Error('Not implemented.');
}

/**
Expand Down Expand Up @@ -111,7 +109,7 @@ export class SharedCache implements Cache {
_request?: RequestInfo,
_options?: SharedCacheQueryOptions
): Promise<readonly Request[]> {
throw new Error('Not Implemented.');
throw new Error('Not implemented.');
}

/**
Expand Down Expand Up @@ -160,23 +158,29 @@ export class SharedCache implements Cache {
}

const fetch = options?._fetch ?? this.#fetch;
const forceCache = options?.forceCache;
const { body, status, statusText } = cacheItem.response;
const policy = CachePolicy.fromObject(cacheItem.policy);

const { body, status, statusText } = cacheItem.response;
const headers = policy.responseHeaders();
let response = new Response(body, {
const stale = policy.stale();
const response = new Response(body, {
status,
statusText,
headers,
});

if (
!forceCache &&
!policy.satisfiesWithoutRevalidation(r, {
ignoreRequestCacheControl: options?.ignoreRequestCacheControl,
ignoreMethod: true,
ignoreSearch: true,
})
ignoreVary: true,
}) ||
stale
) {
if (policy.stale() && policy.useStaleWhileRevalidate()) {
if (!fetch) {
return;
} else if (stale && policy.useStaleWhileRevalidate()) {
// Well actually, in this case it's fine to return the stale response.
// But we'll update the cache in the background.
this.#waitUntil(
Expand All @@ -192,8 +196,9 @@ export class SharedCache implements Cache {
)
);
this.#setCacheStatus(response, STALE);
return response;
} else {
response = await this.#revalidate(
return this.#revalidate(
r,
{
response,
Expand All @@ -204,10 +209,9 @@ export class SharedCache implements Cache {
options
);
}
} else {
this.#setCacheStatus(response, HIT);
}

this.#setCacheStatus(response, HIT);
return response;
}

Expand All @@ -216,7 +220,7 @@ export class SharedCache implements Cache {
_request?: RequestInfo,
_options?: SharedCacheQueryOptions
): Promise<readonly Response[]> {
throw new Error('Not Implemented.');
throw new Error('Not implemented.');
}

/**
Expand Down Expand Up @@ -263,7 +267,7 @@ export class SharedCache implements Cache {
!urlIsHttpHttpsScheme(innerRequest.url) ||
innerRequest.method !== 'GET'
) {
new TypeError(
throw new TypeError(
`Cache.put: Expected an http/s scheme when method is not GET.`
);
}
Expand Down Expand Up @@ -301,6 +305,8 @@ export class SharedCache implements Cache {
// 9.
const clonedResponse = innerResponse.clone();

// TODO: 10. - 19.

const policy = new CachePolicy(innerRequest, clonedResponse);
const ttl = policy.timeToLive();

Expand Down Expand Up @@ -343,10 +349,10 @@ export class SharedCache implements Cache {
): Promise<Response> {
const revalidationRequest = new Request(request, {
headers: resolveCacheItem.policy.revalidationHeaders(request, {
ignoreRequestCacheControl: options?.ignoreRequestCacheControl ?? true,
ignoreRequestCacheControl: options?.ignoreRequestCacheControl,
ignoreMethod: true,
ignoreSearch: true,
ignoreVary: false,
ignoreVary: true,
}),
});
let revalidationResponse: Response;
Expand Down
40 changes: 37 additions & 3 deletions src/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe('multiple duplicate requests', () => {
});
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('text/lol; charset=utf-8');
expect(res.headers.get('x-cache-status')).toBe(MISS);
expect(res.headers.get('x-cache-status')).toBe(DYNAMIC);
expect(res.headers.get('etag')).toBe('"v1"');
expect(await res.text()).toBe('lol');
});
Expand Down Expand Up @@ -194,7 +194,7 @@ test('when no cache control is set the latest content should be loaded', async (
expect(await res.text()).toBe('lol');
});

test('should respect cache control directives from requests', async () => {
test.only('should respect cache control directives from requests', async () => {
const store = createCacheStore();
const cache = new SharedCache(store);
const fetch = createSharedCacheFetch(cache, {
Expand All @@ -210,6 +210,9 @@ test('should respect cache control directives from requests', async () => {
headers: {
'cache-control': 'no-cache',
},
sharedCache: {
ignoreRequestCacheControl: false,
},
});

expect(res.status).toBe(200);
Expand All @@ -222,6 +225,9 @@ test('should respect cache control directives from requests', async () => {
headers: {
'cache-control': 'no-cache',
},
sharedCache: {
ignoreRequestCacheControl: false,
},
});

expect(res.status).toBe(200);
Expand Down Expand Up @@ -251,6 +257,34 @@ test('when body is a string it should cache the response', async () => {
expect(await cachedRes?.text()).toBe('lol');
});

test('when the method is HEAD, it should read the cache of the GET request', async () => {
const store = createCacheStore();
const cache = new SharedCache(store);
const fetch = createSharedCacheFetch(cache, {
async fetch(input, init) {
const req = new Request(input, init);
return new Response(req.method, {
headers: {
'cache-control': 'max-age=300',
},
});
},
});
const get = new Request(TEST_URL, {
method: 'GET',
});
await fetch(get);
const head = new Request(TEST_URL, {
method: 'HEAD',
});
const res = await fetch(head);

expect(res.status).toBe(200);
expect(await res.text()).toBe('GET');
expect(res.headers.get('x-cache-status')).toBe(HIT);
expect(await cache.match(head)).toBeUndefined();
});

test('when the method is POST it should not cache the response', async () => {
const store = createCacheStore();
const cache = new SharedCache(store);
Expand All @@ -275,7 +309,7 @@ test('when the method is POST it should not cache the response', async () => {

expect(res.status).toBe(200);
expect(await res.text()).toBe('POST');
expect(res.headers.get('x-cache-status')).toBe(MISS);
expect(res.headers.get('x-cache-status')).toBe(DYNAMIC);
expect(await cache.match(post)).toBeUndefined();
});

Expand Down
Loading