Skip to content

Commit

Permalink
Tweaked the types a little to make them docs friendly
Browse files Browse the repository at this point in the history
  • Loading branch information
rbshop committed Oct 11, 2024
1 parent 5e300d2 commit 5545b9e
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 110 deletions.
23 changes: 21 additions & 2 deletions packages/hydrogen/src/cache/create-with-cache.doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,27 @@ const data: ReferenceEntityTemplateSchema = {
subCategory: 'caching',
isVisualComponent: false,
related: [],
description:
'Creates utility functions to store data in cache with stale-while-revalidate support.\n - Use `withCache.fetch` to simply fetch data from a third-party API.\n - Use the more advanced `withCache.run` to execute any asynchronous operation.',
description: `Creates utility functions to store data in cache with stale-while-revalidate support.
- Use \`withCache.fetch\` to simply fetch data from a third-party API.
Fetches data from a URL and caches the result according to the strategy provided.
When the response is not successful (e.g. status code >= 400), the caching is
skipped automatically and the returned \`data\` is \`null\`.
You can also prevent caching by using the \`shouldCacheResponse\` option and returning
\`false\` from the function you pass in. For example, you might want to fetch data from a
third-party GraphQL API but not cache the result if the GraphQL response body contains errors.
- Use the more advanced \`withCache.run\` to execute any asynchronous operation.
Utility function that executes asynchronous operations and caches the
result according to the strategy provided. Use this to do any type
of asynchronous operation where \`withCache.fetch\` is insufficient.
For example, when making multiple calls to a third-party API where the
result of all of them needs to be cached under the same cache key.
Whatever data is returned from the \`fn\` will be cached according
to the strategy provided.
> Note:
> To prevent caching the result you must throw an error. Otherwise, the result will be cached.
> For example, if you call \`fetch\` but the response is not successful (e.g. status code >= 400),
> you should throw an error. Otherwise, the response will be cached.
`,
type: 'utility',
defaultExample: {
description: 'I am the default example',
Expand Down
16 changes: 9 additions & 7 deletions packages/hydrogen/src/cache/create-with-cache.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default {
// Optionally, specify a cache strategy.
// Default is CacheShort().
cache: CacheLong(),
// Cache if there are no GralhQL errors:
// Cache if there are no GraphQL errors:
shouldCacheResponse: (body) => !body?.errors,
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
Expand All @@ -52,11 +52,13 @@ export default {
// 2. Or Create a more advanced utility to query multiple APIs under the same cache key:
const fetchMultipleCMS = (options: {id: string; handle: string}) => {
// Prefix the cache key and make it unique based on arguments.
return withCache.run({
cacheKey: ['my-cms-composite', options.id, options.handle],
strategy: CacheLong(),
shouldCacheResult: () => true,
actionFn: async (params) => {
return withCache.run(
{
cacheKey: ['my-cms-composite', options.id, options.handle],
strategy: CacheLong(),
shouldCacheResult: () => true,
},
async (params) => {
// Run multiple subrequests in parallel, or any other async operations.
const [response1, response2] = await Promise.all([
fetch('https://my-cms-1.com/api', {
Expand Down Expand Up @@ -97,7 +99,7 @@ export default {
extra2: response2.headers.get('X-Extra'),
};
},
});
);
};

const handleRequest = createRequestHandler({
Expand Down
120 changes: 70 additions & 50 deletions packages/hydrogen/src/cache/create-with-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,29 @@ describe('createWithCache', () => {

it('skips cache for no-cache policy', async () => {
await expect(
withCache.run({
cacheKey: KEY,
strategy: CacheNone(),
shouldCacheResult: () => true,
withCache.run(
{
cacheKey: KEY,
strategy: CacheNone(),
shouldCacheResult: () => true,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);

expect(waitUntil).toHaveBeenCalledTimes(0);
expect(actionFn).toHaveBeenCalledTimes(1);
await expect(getItemFromCache(cache, KEY)).resolves.toEqual(undefined);

await expect(
withCache.run({
cacheKey: KEY,
strategy: CacheNone(),
shouldCacheResult: () => true,
withCache.run(
{
cacheKey: KEY,
strategy: CacheNone(),
shouldCacheResult: () => true,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);

// No cache, always calls the action function:
Expand All @@ -71,12 +75,14 @@ describe('createWithCache', () => {
});

await expect(
withCache.run({
cacheKey: KEY,
strategy: CacheShort(),
shouldCacheResult: () => true,
withCache.run(
{
cacheKey: KEY,
strategy: CacheShort(),
shouldCacheResult: () => true,
},
actionFn,
}),
),
).rejects.toThrowError('test');

expect(waitUntil).toHaveBeenCalledTimes(0);
Expand All @@ -87,25 +93,29 @@ describe('createWithCache', () => {
it('skips cache when shouldCacheResult returns false', async () => {
const strategy = CacheShort({maxAge: 1, staleWhileRevalidate: 9});
await expect(
withCache.run({
cacheKey: KEY,
strategy,
shouldCacheResult: (v) => v !== VALUE,
withCache.run(
{
cacheKey: KEY,
strategy,
shouldCacheResult: (v) => v !== VALUE,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);

expect(waitUntil).toHaveBeenCalledTimes(0);
expect(actionFn).toHaveBeenCalledTimes(1);
await expect(getItemFromCache(cache, KEY)).resolves.toEqual(undefined);

await expect(
withCache.run({
cacheKey: KEY,
strategy,
shouldCacheResult: (v) => v !== VALUE,
withCache.run(
{
cacheKey: KEY,
strategy,
shouldCacheResult: (v) => v !== VALUE,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);

// Doesn't cache, so always runs the actionFn:
Expand All @@ -117,12 +127,14 @@ describe('createWithCache', () => {
it('stores results in the cache', async () => {
const strategy = CacheShort({maxAge: 1, staleWhileRevalidate: 9});
await expect(
withCache.run({
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
withCache.run(
{
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);

expect(waitUntil).toHaveBeenCalledTimes(1);
Expand All @@ -135,12 +147,14 @@ describe('createWithCache', () => {
vi.advanceTimersByTime(999);

await expect(
withCache.run({
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
withCache.run(
{
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);

// Cache hit, nothing to update:
Expand All @@ -154,12 +168,14 @@ describe('createWithCache', () => {
it('applies stale-while-revalidate', async () => {
const strategy = CacheShort({maxAge: 1, staleWhileRevalidate: 9});
await expect(
withCache.run({
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
withCache.run(
{
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);

expect(waitUntil).toHaveBeenCalledTimes(1);
Expand All @@ -172,12 +188,14 @@ describe('createWithCache', () => {
vi.advanceTimersByTime(3000);

await expect(
withCache.run({
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
withCache.run(
{
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);

// Cache stale, call the action function again for SWR:
Expand All @@ -196,12 +214,14 @@ describe('createWithCache', () => {

// Cache is expired, call the action function again:
await expect(
withCache.run({
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
withCache.run(
{
cacheKey: KEY,
strategy,
shouldCacheResult: () => true,
},
actionFn,
}),
),
).resolves.toEqual(VALUE);
expect(waitUntil).toHaveBeenCalledTimes(3);
expect(actionFn).toHaveBeenCalledTimes(3);
Expand Down
95 changes: 44 additions & 51 deletions packages/hydrogen/src/cache/create-with-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import {
CacheActionFunctionParam,
CacheKey,
runWithCache,
type DebugOptions,
} from './run-with-cache';
import {fetchWithServerCache, type FetchCacheOptions} from './server-fetch';
import {fetchWithServerCache} from './server-fetch';
import type {WaitUntil} from '../types';

type CreateWithCacheOptions = {
Expand All @@ -19,46 +18,54 @@ type CreateWithCacheOptions = {
request?: CrossRuntimeRequest;
};

type RunOptions<T> = {
type WithCacheRunOptions<T> = {
/** The cache key for this run */
cacheKey: CacheKey;
/**
* Use the `CachingStrategy` to define a custom caching mechanism for your data.
* Or use one of the pre-defined caching strategies: [`CacheNone`](/docs/api/hydrogen/utilities/cachenone), [`CacheShort`](/docs/api/hydrogen/utilities/cacheshort), [`CacheLong`](/docs/api/hydrogen/utilities/cachelong).
*/
strategy: CachingStrategy;
/** Useful to avoid accidentally caching bad results */
shouldCacheResult: (value: T) => boolean;
actionFn: ({addDebugData}: CacheActionFunctionParam) => T | Promise<T>;
};

/**
* Creates utility functions to store data in cache with stale-while-revalidate support.
* - Use `withCache.fetch` to simply fetch data from a third-party API.
* - Use the more advanced `withCache.run` to execute any asynchronous operation.
*/
export function createWithCache<T = unknown>(
type WithCacheFetchOptions<T> = {
displayName?: string;
/**
* Use the `CachingStrategy` to define a custom caching mechanism for your data.
* Or use one of the pre-defined caching strategies: [`CacheNone`](/docs/api/hydrogen/utilities/cachenone), [`CacheShort`](/docs/api/hydrogen/utilities/cacheshort), [`CacheLong`](/docs/api/hydrogen/utilities/cachelong).
*/
cache?: CachingStrategy;
/** The cache key for this fetch */
cacheKey?: CacheKey;
/** Useful to avoid e.g. caching a successful response that contains an error in the body */
shouldCacheResponse: (body: T, response: Response) => boolean;
};

export type WithCache = {
run: <T>(
options: WithCacheRunOptions<T>,
fn: ({addDebugData}: CacheActionFunctionParam) => T | Promise<T>,
) => Promise<T>;
fetch: <T>(
url: string,
requestInit: RequestInit,
options: WithCacheFetchOptions<T>,
) => Promise<{data: T | null; response: Response}>;
};

export function createWithCache(
cacheOptions: CreateWithCacheOptions,
) {
): WithCache {
const {cache, waitUntil, request} = cacheOptions;

return {
/**
* Utility function that executes asynchronous operations and caches the
* result according to the strategy provided. Use this to do any type
* of asynchronous operation where `withCache.fetch` is insufficient.
* For example, when making multiple calls to a third-party API where the
* result of all of them needs to be cached under the same cache key.
* Whatever data is returned from the `actionFn` will be cached according
* to the strategy provided.
* Use the `CachingStrategy` to define a custom caching mechanism for your data.
* Or use one of the built-in caching strategies: `CacheNone`, `CacheShort`, `CacheLong`.
* > Note:
* > To prevent caching the result you must throw an error. Otherwise, the result will be cached.
* > For example, if you call `fetch` but the response is not successful (e.g. status code >= 400),
* > you should throw an error. Otherwise, the response will be cached.
*/
run<InferredActionReturn = T>({
cacheKey,
strategy,
shouldCacheResult,
actionFn,
}: RunOptions<InferredActionReturn>) {
return runWithCache(cacheKey, actionFn, {
run: <T>(
{cacheKey, strategy, shouldCacheResult}: WithCacheRunOptions<T>,
fn: ({addDebugData}: CacheActionFunctionParam) => T | Promise<T>,
): Promise<T> => {
return runWithCache(cacheKey, fn, {
shouldCacheResult,
strategy,
cacheInstance: cache,
Expand All @@ -70,24 +77,12 @@ export function createWithCache<T = unknown>(
});
},

/**
* Fetches data from a URL and caches the result according to the strategy provided.
* When the response is not successful (e.g. status code >= 400), the caching is
* skipped automatically and the returned `data` is `null`.
* You can also prevent caching by using the `shouldCacheResponse` option and returning
* `false` from the function you pass in. For example, you might want to fetch data from a
* third-party GraphQL API but not cache the result if the GraphQL response body contains errors.
*/
fetch<Body = T>(
fetch: <T>(
url: string,
requestInit: RequestInit,
options: Pick<DebugOptions, 'displayName'> &
Pick<
FetchCacheOptions<Body>,
'cache' | 'cacheKey' | 'shouldCacheResponse'
>,
): Promise<{data: Body | null; response: Response}> {
return fetchWithServerCache<Body | null>(url, requestInit ?? {}, {
options: WithCacheFetchOptions<T>,
): Promise<{data: T | null; response: Response}> => {
return fetchWithServerCache<T | null>(url, requestInit ?? {}, {
waitUntil,
cacheKey: [url, requestInit],
cacheInstance: cache,
Expand All @@ -102,5 +97,3 @@ export function createWithCache<T = unknown>(
},
};
}

export type WithCache = ReturnType<typeof createWithCache>;

0 comments on commit 5545b9e

Please sign in to comment.