Skip to content

Commit

Permalink
update example and changeset snippet
Browse files Browse the repository at this point in the history
  • Loading branch information
wizardlyhel committed Oct 23, 2024
1 parent 19fd3cf commit ea8c4b0
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 89 deletions.
65 changes: 62 additions & 3 deletions .changeset/serious-pillows-hug.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,66 @@
'@shopify/hydrogen': patch
---

Update `createWithCache` to make it harder to accidentally cache undesired results. `createWithCache` now returns an object with two utility functions:
[**Breaking change**]

- `withCache.fetch` to be used for fetch requests
- the more advanced `withCache.run` to execute any asynchronous operation
Update `createWithCache` to make it harder to accidentally cache undesired results. `request` is now mandatory prop when initializing `createWithCache`.

```diff
// server.ts
export default {
async fetch(
request: Request,
env: Env,
executionContext: ExecutionContext,
): Promise<Response> {
try {
// ...
- const withCache = createWithCache({cache, waitUntil});
+ const withCache = createWithCache({cache, waitUntil, request});
```

`createWithCache` now returns an object with two utility functions. The original `withCache` callback function is now `withCache.run`.

```diff
const withCache = createWithCache({cache, waitUntil, request});

const fetchMyCMS = (query) => {
- return withCache(['my-cms', query], CacheLong(), async (params) => {
+ return withCache.run({
+ cacheKey: ['my-cms-composite', query],
+ cacheStrategy: CacheLong(),
+ shouldCacheResult: (body) => !body?.errors,
+ }, async(params) => {
const response = await fetch('my-cms.com/api', {
method: 'POST',
body: query,
});
if (!response.ok) throw new Error(response.statusText);
const {data, error} = await response.json();
if (error || !data) throw new Error(error ?? 'Missing data');
params.addDebugData({displayName: 'My CMS query', response});
return data;
});
};
```

`withCache.fetch` is be used for caching simple fetch requests:

```ts
const withCache = createWithCache({cache, waitUntil, request});

const {data, response} = await withCache.fetch<{data: T; error: string}>(
'my-cms.com/api',
{
method: 'POST',
headers: {'Content-type': 'application/json'},
body,
},
{
cacheStrategy: options.cache ?? CacheLong(),
shouldCacheResponse: (body) => !body?.error,
cacheKey: ['my-cms', body],
displayName: 'My CMS query',
},
);
```
85 changes: 44 additions & 41 deletions packages/hydrogen/src/cache/create-with-cache.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export default {
{
// Optionally, specify a cache strategy.
// Default is CacheShort().
cache: CacheLong(),
// Cache if there are no GralhQL errors:
shouldCacheResponse: (body) => !body?.errors,
cacheStrategy: CacheLong(),
// Cache if there are no GralhQL errors or a specific result that make this result not suited for caching:
shouldCacheResponse: (result) => !(result?.errors || result?.isLoggedIn),
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
displayName: 'My CMS query',
Expand All @@ -44,50 +44,53 @@ export default {
const fetchMultipleCMS = (options) => {
// Prefix the cache key and make it unique based on arguments.
return withCache.run({
// Define a cache key that is unique to this query
cacheKey: ['my-cms-composite', options.id, options.handle],
strategy: CacheLong(),
shouldCacheResult: () => true,
actionFn: 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', {
method: 'POST',
body: JSON.stringify({id: options.id}),
}),
fetch('https://my-cms-2.com/api', {
method: 'POST',
body: JSON.stringify({handle: options.handle}),
}),
]);
// Optionally, specify a cache strategy.
// Default is CacheShort().
cacheStrategy: CacheLong(),
// Cache if there are no GralhQL errors or a specific result that make this result not suited for caching:
shouldCacheResponse: (result) => !(result?.errors || result?.isLoggedIn),
}, 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', {
method: 'POST',
body: JSON.stringify({id: options.id}),
}),
fetch('https://my-cms-2.com/api', {
method: 'POST',
body: JSON.stringify({handle: options.handle}),
}),
]);

// Throw if any response is unsuccessful.
// This is important to prevent the results from being cached.
if (!response1.ok || !response2.ok) {
throw new Error('Failed to fetch data');
}
// Throw if any response is unsuccessful.
// This is important to prevent the results from being cached.
if (!response1.ok || !response2.ok) {
throw new Error('Failed to fetch data');
}

const [data1, data2] = await Promise.all([
response1.json(),
response2.json(),
]);
const [data1, data2] = await Promise.all([
response1.json(),
response2.json(),
]);

// Validate data and throw to avoid caching errors.
if (data1.errors || data2.errors) {
throw new Error('API errors');
}
// Validate data and throw to avoid caching errors.
if (data1.errors || data2.errors) {
throw new Error('API errors');
}

// Optionally, add extra information to show
// in the Subrequest Profiler utility.
params.addDebugData({displayName: 'My CMS query'});
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
params.addDebugData({displayName: 'My CMS query'});

// Compose the result as needed.
return {
...data1,
...data2,
extra1: response1.headers.get('X-Extra'),
extra2: response2.headers.get('X-Extra'),
};
},
// Compose the result as needed.
return {
...data1,
...data2,
extra1: response1.headers.get('X-Extra'),
extra2: response2.headers.get('X-Extra'),
};
});
};

Expand Down
100 changes: 55 additions & 45 deletions packages/hydrogen/src/cache/create-with-cache.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ export default {

type ExpectedResponse = {
content: unknown;
isLoggedIn: boolean;
errors?: string;
};

type MergedResponse = {
content: unknown;
isLoggedIn: boolean;
errors?: string;
extra1: string | null;
extra2: string | null;
};

// 1. Create a custom utility to query a third-party API:
const fetchMyCMS = async (query: string) => {
const {data, response} = await withCache.fetch<ExpectedResponse>(
Expand All @@ -35,8 +44,8 @@ export default {
// Optionally, specify a cache strategy.
// Default is CacheShort().
cacheStrategy: CacheLong(),
// Cache if there are no GraphQL errors:
shouldCacheResponse: (body) => !body?.errors,
// Cache if there are no GralhQL errors or a specific result that make this result not suited for caching:
shouldCacheResponse: (result) => !(result?.errors || result?.isLoggedIn),
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
displayName: 'My CMS query',
Expand All @@ -52,54 +61,55 @@ 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],
cacheStrategy: 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', {
method: 'POST',
body: JSON.stringify({id: options.id}),
}),
fetch('https://my-cms-2.com/api', {
method: 'POST',
body: JSON.stringify({handle: options.handle}),
}),
]);
return withCache.run({
// Define a cache key that is unique to this query
cacheKey: ['my-cms-composite', options.id, options.handle],
// Optionally, specify a cache strategy.
// Default is CacheShort().
cacheStrategy: CacheLong(),
// Cache if there are no GralhQL errors or a specific result that make this result not suited for caching:
shouldCacheResult: (result: MergedResponse) => !(result?.errors || result?.isLoggedIn),
}, 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', {
method: 'POST',
body: JSON.stringify({id: options.id}),
}),
fetch('https://my-cms-2.com/api', {
method: 'POST',
body: JSON.stringify({handle: options.handle}),
}),
]);

// Throw if any response is unsuccessful.
// This is important to prevent the results from being cached.
if (!response1.ok || !response2.ok) {
throw new Error('Failed to fetch data');
}
// Throw if any response is unsuccessful.
// This is important to prevent the results from being cached.
if (!response1.ok || !response2.ok) {
throw new Error('Failed to fetch data');
}

const [data1, data2] = (await Promise.all([
response1.json(),
response2.json(),
])) as [ExpectedResponse, ExpectedResponse];
const [data1, data2] = (await Promise.all([
response1.json(),
response2.json(),
])) as [ExpectedResponse, ExpectedResponse];

// Validate data and throw to avoid caching errors.
if (data1.errors || data2.errors) {
throw new Error('API errors');
}
// Validate data and throw to avoid caching errors.
if (data1.errors || data2.errors) {
throw new Error('API errors');
}

// Optionally, add extra information to show
// in the Subrequest Profiler utility.
params.addDebugData({displayName: 'My CMS query'});
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
params.addDebugData({displayName: 'My CMS query'});

// Compose the result as needed.
return {
...data1,
...data2,
extra1: response1.headers.get('X-Extra'),
extra2: response2.headers.get('X-Extra'),
};
},
);
// Compose the result as needed.
return {
...data1,
...data2,
extra1: response1.headers.get('X-Extra'),
extra2: response2.headers.get('X-Extra'),
} as MergedResponse;
});
};

const handleRequest = createRequestHandler({
Expand Down

0 comments on commit ea8c4b0

Please sign in to comment.