Skip to content

Commit

Permalink
feat(context): allow user to specify default (de)serialization at con…
Browse files Browse the repository at this point in the history
…text level
  • Loading branch information
uladkasach committed Nov 25, 2022
1 parent e62a9ab commit 94efac8
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 19 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@
},
"dependencies": {
"type-fns": "^0.4.1",
"with-simple-caching": "^0.9.4"
"with-simple-caching": "^0.9.5"
}
}
67 changes: 65 additions & 2 deletions src/createRemoteStateCachingContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ describe('createRemoteStateCachingContext', () => {
// update a request
await queryGetRecipes.update({
forKey: [queryGetRecipes.name, JSON.stringify([{ searchFor: 'smoothie' }])].join('.'), // update by key, instead of input
toValue: async ({ cachedValue }) => [
...((await cachedValue) ?? []),
toValue: async ({ fromCachedOutput }) => [
...((await fromCachedOutput) ?? []),
{ title: 'new smoothie', description: 'great smothie', ingredients: [], steps: [] }, // add a new recipe to it
],
});
Expand Down Expand Up @@ -404,4 +404,67 @@ describe('createRemoteStateCachingContext', () => {
expect(apiCalls.length).toEqual(3); // should not have had another api call, since we updated the cache, not invalidated it
});
});
describe('(de)serialization', () => {
it('should allow user to specify default context level serialization and deserialization', async () => {
// start the context
const { withRemoteStateQueryCaching } = createRemoteStateCachingContext({
cache: createCache<any>(),
serialize: {
value: async (output) => JSON.stringify(await output),
},
deserialize: {
value: async (cached) => JSON.parse(await cached),
},
});

// define a query that we'll be caching
const apiCalls = [];
const queryGetRecipes = withRemoteStateQueryCaching(
async ({ searchFor }: { searchFor: string }): Promise<Recipe[]> => {
apiCalls.push(searchFor);
return [{ uuid: uuid(), title: '__TITLE__', description: '__DESCRIPTION__', ingredients: [], steps: [] }];
},
{
name: 'queryGetRecipes',
},
);

// make a few requests
const result1 = await queryGetRecipes.execute({ searchFor: 'steak' });
const result2 = await queryGetRecipes.execute({ searchFor: 'smoothie' });
const result3 = await queryGetRecipes.execute({ searchFor: 'steak' });
const result4 = await queryGetRecipes.execute({ searchFor: 'smoothie' });

// prove that subsequent duplicate requests returned the same result
expect(result3).toEqual(result1);
expect(result4).toEqual(result2);

// prove that we only called the api twice, once per unique request, since dupe request responses should have come from cache
expect(apiCalls.length).toEqual(2);

// invalidate a request
await queryGetRecipes.invalidate({ forInput: [{ searchFor: 'steak' }] });

// now make the query again and prove that it hits the api
const result5 = await queryGetRecipes.execute({ searchFor: 'steak' });
expect(result5).not.toEqual(result1);
expect(apiCalls.length).toEqual(3);

// update a request
await queryGetRecipes.update({
forKey: [queryGetRecipes.name, JSON.stringify([{ searchFor: 'smoothie' }])].join('.'), // update by key, instead of input
toValue: async ({ fromCachedOutput }) => [
...((await fromCachedOutput) ?? []),
{ title: 'new smoothie', description: 'great smothie', ingredients: [], steps: [] }, // add a new recipe to it
],
});

// now make a query again and prove that it doesn't hit the api, but returned the updated value
const result6 = await queryGetRecipes.execute({ searchFor: 'smoothie' });
expect(apiCalls.length).toEqual(3); // no increase
expect(result6).not.toEqual(result2); // new value
expect(result6.length).toEqual(2); // should have 2 recipes now
expect(result6[1]).toMatchObject({ title: 'new smoothie' }); // the second should be the one we explicitly added
});
});
});
37 changes: 28 additions & 9 deletions src/createRemoteStateCachingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
defaultValueDeserializationMethod,
defaultKeySerializationMethod,
KeySerializationMethod,
defaultValueSerializationMethod,
} from 'with-simple-caching';
import { RemoteStateCacheContext, RemoteStateCacheContextQueryRegistration } from './RemoteStateCacheContext';
import { RemoteStateQueryInvalidationTrigger, RemoteStateQueryUpdateTrigger } from './RemoteStateQueryCachingOptions';
Expand Down Expand Up @@ -138,7 +139,7 @@ export const createRemoteStateCachingContext = <
* note:
* - if it is too restrictive, you can define a serialize + deserialize method for your function's output w/ options
*/
SCV extends any = any // SCV = shared cache value
SCV extends any // SCV = shared cache value
>({
cache,
...defaultOptions
Expand All @@ -153,9 +154,24 @@ export const createRemoteStateCachingContext = <
*/
serialize?: {
/**
* allow specifying a default serialization key
* allow specifying a default key serialization method
*/
key: KeySerializationMethod<SLI>;
key?: KeySerializationMethod<SLI>;

/**
* allow specifying a default value serialization method
*/
value?: (output: Promise<any>) => SCV;
};

/**
* allow specifying default deserialization options
*/
deserialize?: {
/**
* allow specifying a default value deserialization method
*/
value?: (cached: SCV) => Promise<any>;
};
}) => {
/**
Expand Down Expand Up @@ -190,7 +206,7 @@ export const createRemoteStateCachingContext = <
* - automatically invalidating or updating the cached response for a query, triggered by mutations
* - manually invalidating or updating the cached response for a query
*/
const withRemoteStateQueryCaching = <L extends (...args: any[]) => any, CV extends any = ReturnType<L>>(
const withRemoteStateQueryCaching = <L extends (...args: any[]) => any, CV extends SCV = ReturnType<L>>(
logic: L,
options: Omit<WithSimpleCachingOptions<L, CV>, 'cache'> & WithRemoteStateCachingOptions,
): QueryWithRemoteStateCaching<L, CV> => {
Expand All @@ -207,8 +223,11 @@ export const createRemoteStateCachingContext = <
const logicExtendedWithCaching = withExtendableCaching(logic, {
...options,
serialize: {
...options.serialize,
key: keySerializationMethodWithNamespace,
value: options.serialize?.value ?? (defaultOptions.serialize?.value as any) ?? defaultValueSerializationMethod,
},
deserialize: {
value: options.deserialize?.value ?? (defaultOptions.deserialize?.value as any) ?? defaultValueDeserializationMethod,
},
cache: cache as WithSimpleCachingOptions<L, CV>['cache'], // we've asserted that CV is a subset of SCV, so this in reality will work; // TODO: determine why typescript is not happy here
});
Expand Down Expand Up @@ -244,7 +263,7 @@ export const createRemoteStateCachingContext = <
/**
* define a method which is able to kick off all registered query invalidations and query updates, on the execution of a mutation
*/
const onMutationOutput = async <LO extends any, CV extends any, M extends (...args: any) => any>({
const onMutationOutput = async <LI extends any, LO extends any, M extends (...args: any) => any>({
mutationName,
mutationInput,
mutationOutput,
Expand Down Expand Up @@ -307,11 +326,11 @@ export const createRemoteStateCachingContext = <
});

// define the function that will be used to update the cache with
const toValue: (args: { cachedValue: CV | undefined }) => LO = ({ cachedValue }) =>
cachedValue // only run the update if the cache is still valid for this key; otherwise, it shouldn't have been called; i.e., sheild the trigger function from invalidated, undefined, cache values
const toValue: (args: { fromCachedOutput: LI | undefined }) => LO = ({ fromCachedOutput }) =>
fromCachedOutput // only run the update if the cache is still valid for this key; otherwise, it shouldn't have been called; i.e., sheild the trigger function from invalidated, undefined, cache values
? updatedByThisMutationDefinition.update({
from: {
cachedQueryOutput: Promise.resolve(cachedValue).then(registration.options.deserialize.value), // ensure to wrap it in a promise, so that even if a sync cache is used, the result is consistent w/ output type
cachedQueryOutput: Promise.resolve(fromCachedOutput), // ensure to wrap it in a promise, so that even if a sync cache is used, the result is consistent w/ output type
},
with: {
mutationInput,
Expand Down

0 comments on commit 94efac8

Please sign in to comment.