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

[HACKDAYS] O2 cache #2283

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 22 additions & 15 deletions packages/hydrogen/src/cache/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,7 @@ async function getItem(
return response;
}

/**
* Put an item into the cache.
*/
async function setItem(
cache: Cache,
request: Request,
response: Response,
userCacheOptions: CachingStrategy,
) {
if (!cache) return;

function getCacheControlHeaders(userCacheOptions: CachingStrategy) {
/**
* We are manually managing staled request by adding this workaround.
* Why? cache control header support is dependent on hosting platform
Expand Down Expand Up @@ -129,9 +119,26 @@ async function setItem(

// CF will override cache-control, so we need to keep a non-modified real-cache-control
// cache-control is still necessary for mini-oxygen
response.headers.set('cache-control', paddedCacheControlString);
response.headers.set('real-cache-control', cacheControlString);
response.headers.set('cache-put-date', String(Date.now()));
return [
['cache-control', paddedCacheControlString],
['real-cache-control', cacheControlString],
['cache-put-date', String(Date.now())],
];
}
/**
* Put an item into the cache.
*/
async function setItem(
cache: Cache,
request: Request,
response: Response,
userCacheOptions: CachingStrategy,
) {
if (!cache) return;

for (const [key, value] of getCacheControlHeaders(userCacheOptions)) {
response.headers.set(key, value);
}

logCacheApiStatus('PUT', request, response);
await cache.put(request, response);
Expand Down Expand Up @@ -187,6 +194,6 @@ export const CacheAPI = {
get: getItem,
set: setItem,
delete: deleteItem,
generateDefaultCacheControlHeader,
getCacheControlHeaders,
isStale,
};
39 changes: 24 additions & 15 deletions packages/hydrogen/src/cache/run-with-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import {
generateCacheControlHeader,
type CachingStrategy,
} from './strategies';
import {
getItemFromCache,
getKeyUrl,
isStale,
setItemInCache,
} from './sub-request';
import {getItemFromCache, getKeyUrl, setItemInCache} from './sub-request';
import {type StackInfo} from '../utils/callsites';
import {hashKey} from '../utils/hash';
import type {WaitUntil} from '../types';
Expand Down Expand Up @@ -53,6 +48,7 @@ type WithCacheOptions<T = unknown> = {
shouldCacheResult?: (value: T) => boolean;
waitUntil?: WaitUntil;
debugInfo?: DebugOptions;
cacheTags?: string[];
};

// Lock to prevent revalidating the same sub-request
Expand All @@ -75,14 +71,22 @@ export async function runWithCache<T = unknown>(
shouldCacheResult = () => true,
waitUntil,
debugInfo,
cacheTags,
}: WithCacheOptions<T>,
): Promise<T> {
const startTime = Date.now();
const key = hashKey([
const key = await hashKey([
// '__HYDROGEN_CACHE_ID__', // TODO purgeQueryCacheOnBuild
...(typeof cacheKey === 'string' ? [cacheKey] : cacheKey),
]);

// console.debug(key, {
// original: (Array.isArray(cacheKey) ? cacheKey.join(':') : cacheKey).slice(
// 170,
// 270,
// ),
// });

let cachedDebugInfo: CachedDebugInfo | undefined;
let userDebugInfo: CachedDebugInfo | undefined;

Expand Down Expand Up @@ -135,6 +139,7 @@ export async function runWithCache<T = unknown>(
status: cacheStatus,
strategy: generateCacheControlHeader(strategy || {}),
key,
tags: cacheTags,
},
waitUntil,
});
Expand Down Expand Up @@ -163,16 +168,18 @@ export async function runWithCache<T = unknown>(
process.env.NODE_ENV === 'development' ? mergeDebugInfo() : undefined,
} satisfies CachedItem,
strategy,
cacheTags,
);

const cachedItem = await getItemFromCache<CachedItem>(cacheInstance, key);
// console.log('--- Cache', cachedItem ? 'HIT' : 'MISS');
const {value: cachedItem, status: cacheStatus} =
await getItemFromCache<CachedItem>(cacheInstance, key);
// console.log('--- Cache', cacheStatus);

if (cachedItem && typeof cachedItem[0] !== 'string') {
const [{value: cachedResult, debugInfo}, cacheInfo] = cachedItem;
cachedDebugInfo = debugInfo;
if (cachedItem && cacheStatus !== 'MISS') {
cachedDebugInfo = cachedItem.debugInfo;
const cachedValue = cachedItem.value;

const cacheStatus = isStale(key, cacheInfo) ? 'STALE' : 'HIT';
console.debug(`CACHE ${cacheStatus}`);

if (!swrLock.has(key) && cacheStatus === 'STALE') {
swrLock.add(key);
Expand Down Expand Up @@ -210,13 +217,15 @@ export async function runWithCache<T = unknown>(

// Log HIT/STALE requests
logSubRequestEvent?.({
result: cachedResult,
result: cachedValue,
cacheStatus,
});

return cachedResult;
return cachedValue;
}

console.debug('CACHE MISS');

const result = await actionFn({addDebugData});

// Log MISS requests
Expand Down
3 changes: 3 additions & 0 deletions packages/hydrogen/src/cache/server-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type FetchCacheOptions = {
cache?: CachingStrategy;
cacheInstance?: Cache;
cacheKey?: CacheKey;
cacheTags?: string[];
shouldCacheResponse?: (body: any, response: Response) => boolean;
waitUntil?: WaitUntil;
returnType?: 'json' | 'text' | 'arrayBuffer' | 'blob';
Expand Down Expand Up @@ -48,6 +49,7 @@ export async function fetchWithServerCache(
cacheInstance,
cache: cacheOptions,
cacheKey = [url, requestInit],
cacheTags,
shouldCacheResponse = () => true,
waitUntil,
returnType = 'json',
Expand Down Expand Up @@ -81,6 +83,7 @@ export async function fetchWithServerCache(
cacheInstance,
waitUntil,
strategy: cacheOptions ?? null,
cacheTags,
debugInfo,
shouldCacheResult: (result) =>
shouldCacheResponse(...fromSerializableResponse(result)),
Expand Down
32 changes: 16 additions & 16 deletions packages/hydrogen/src/cache/sub-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,14 @@ import {
* Cache API is weird. We just need a full URL, so we make one up.
*/
export function getKeyUrl(key: string) {
return `https://shopify.dev/?${key}`;
return `https://shopify.dev/?${encodeURIComponent(key)}`;
}

function getCacheOption(userCacheOptions?: CachingStrategy): AllCacheOptions {
return userCacheOptions || CacheDefault();
}

export function generateSubRequestCacheControlHeader(
userCacheOptions?: CachingStrategy,
): string {
return CacheAPI.generateDefaultCacheControlHeader(
getCacheOption(userCacheOptions),
);
}
type CacheStatus = 'HIT' | 'MISS' | 'STALE';

/**
* Get an item from the cache. If a match is found, returns a tuple
Expand All @@ -38,22 +32,27 @@ export function generateSubRequestCacheControlHeader(
export async function getItemFromCache<T = any>(
cache: Cache,
key: string,
): Promise<undefined | [T | string, Response]> {
if (!cache) return;
): Promise<{value?: T; status: CacheStatus}> {
if (!cache) return {status: 'MISS'};

const url = getKeyUrl(key);
const request = new Request(url);

const response = await CacheAPI.get(cache, request);

if (!response) {
return;
}
if (!response) return {status: 'MISS'};

const text = await response.text();
try {
return [parseJSON(text), response];
const text = await response.text();
return {
value: text ? parseJSON(text) : undefined,
status: text
? (response.headers.get('oxygen-cache-status') as null | CacheStatus) ??
(isStale(key, response) ? 'STALE' : 'HIT')
: 'MISS',
};
} catch {
return [text, response];
return {value: undefined, status: 'MISS'};
}
}

Expand All @@ -66,6 +65,7 @@ export async function setItemInCache(
key: string,
value: any,
userCacheOptions?: CachingStrategy,
tags?: string[],
) {
if (!cache) return;

Expand Down
46 changes: 35 additions & 11 deletions packages/hydrogen/src/storefront.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@shopify/hydrogen-react';
import type {WritableDeep} from 'type-fest';
import {fetchWithServerCache, checkGraphQLErrors} from './cache/server-fetch';
import type {CacheKey} from './cache/run-with-cache';
import {
SDK_VARIANT_HEADER,
SDK_VARIANT_SOURCE_HEADER,
Expand Down Expand Up @@ -118,7 +119,8 @@ export type Storefront<TI18n extends I18nBase = I18nBase> = {
...options: ClientVariablesInRestParams<
StorefrontQueries,
RawGqlString,
StorefrontCommonExtraParams & Pick<StorefrontQueryOptions, 'cache'>,
StorefrontCommonExtraParams &
Pick<StorefrontQueryOptions, 'cache' | 'cacheKey'>,
AutoAddedVariableNames
>
) => Promise<
Expand Down Expand Up @@ -193,13 +195,28 @@ type StorefrontHeaders = {
type StorefrontQueryOptions = StorefrontCommonExtraParams & {
query: string;
mutation?: never;
/**
* Cache strategy.
*/
cache?: CachingStrategy;
/**
* Unique key used for caching. Specify it only if you want
* to be able to manually invalidate by cache key later.
*/
cacheKey?: CacheKey;
/**
* Tags to associate with this query. Useful to invalidate
* the cache by specifying one of the tags.
*/
cacheTags?: string[];
};

type StorefrontMutationOptions = StorefrontCommonExtraParams & {
query?: never;
mutation: string;
cache?: never;
cacheKey?: never;
cacheTags?: never;
};

const defaultI18n: I18nBase = {language: 'EN', country: 'US'};
Expand Down Expand Up @@ -281,6 +298,8 @@ export function createStorefrontClient<TI18n extends I18nBase>(
storefrontApiVersion,
displayName,
stackInfo,
cacheKey,
cacheTags,
}: {variables?: GenericVariables; stackInfo?: StackInfo} & (
| StorefrontQueryOptions
| StorefrontMutationOptions
Expand Down Expand Up @@ -316,18 +335,23 @@ export function createStorefrontClient<TI18n extends I18nBase>(
body: graphqlData,
} satisfies RequestInit;

const cacheKey = [
url,
requestInit.method,
cacheKeyHeader,
requestInit.body,
];
const cacheParams = mutation
? undefined
: {
cacheInstance: cache,
cache: cacheOptions ?? CacheDefault(),
cacheKey: cacheKey ?? [
url,
requestInit.method,
cacheKeyHeader,
requestInit.body,
],
cacheTags,
shouldCacheResponse: checkGraphQLErrors,
};

const [body, response] = await fetchWithServerCache(url, requestInit, {
cacheInstance: mutation ? undefined : cache,
cache: cacheOptions || CacheDefault(),
cacheKey,
shouldCacheResponse: checkGraphQLErrors,
...cacheParams,
waitUntil,
debugInfo: {
requestId: requestInit.headers[STOREFRONT_REQUEST_GROUP_ID_HEADER],
Expand Down
17 changes: 15 additions & 2 deletions packages/hydrogen/src/utils/hash.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
type QueryKey = string | readonly unknown[];

export function hashKey(queryKey: QueryKey): string {
const encoder = new TextEncoder();

export async function hashKey(queryKey: QueryKey): Promise<string> {
const rawKeys = Array.isArray(queryKey) ? queryKey : [queryKey];
let hash = '';

Expand All @@ -21,5 +23,16 @@ export function hashKey(queryKey: QueryKey): string {
}
}

return encodeURIComponent(hash);
const hashBuffer = await crypto.subtle.digest(
'sha-512',
encoder.encode(hash),
);

// Hex string
// return Array.from(new Uint8Array(hashBuffer))
// .map((byte) => byte.toString(16).padStart(2, '0'))
// .join('');

// B64 string
return btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
}
1 change: 1 addition & 0 deletions packages/hydrogen/src/vite/request-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type RequestEventPayload = {
status?: string;
strategy?: string;
key?: string | readonly unknown[];
tags?: string[];
};
displayName?: string;
};
Expand Down
Loading
Loading