From 94efac8d00fb27b766b2b964512680afdb134cd4 Mon Sep 17 00:00:00 2001 From: Ulad Kasach Date: Fri, 25 Nov 2022 10:54:01 -0500 Subject: [PATCH] feat(context): allow user to specify default (de)serialization at context level --- package-lock.json | 14 ++--- package.json | 2 +- src/createRemoteStateCachingContext.test.ts | 67 ++++++++++++++++++++- src/createRemoteStateCachingContext.ts | 37 +++++++++--- 4 files changed, 101 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0f18a0..c9ba19a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "type-fns": "^0.4.1", - "with-simple-caching": "^0.9.4" + "with-simple-caching": "^0.9.5" }, "devDependencies": { "@types/jest": "^27.0.0", @@ -8297,9 +8297,9 @@ } }, "node_modules/with-simple-caching": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/with-simple-caching/-/with-simple-caching-0.9.4.tgz", - "integrity": "sha512-0PKrPcE5bSTLDTbUkNsUI1avt0AoNECaWvdTr7dG6HZHqJJ+PosjHgHK0Xac/hP/oWuk0igWDjnQCHDSt1rkNg==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/with-simple-caching/-/with-simple-caching-0.9.5.tgz", + "integrity": "sha512-C99GmV+eHNuFoHIErEc5gKZ8QWVdx8KopvqjcP96rd6IaeKGp6iRy4R6sgRImdXaR8/K/hk6C2fyP1QKlgBrJw==", "dependencies": { "type-fns": "^0.4.1" }, @@ -14808,9 +14808,9 @@ } }, "with-simple-caching": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/with-simple-caching/-/with-simple-caching-0.9.4.tgz", - "integrity": "sha512-0PKrPcE5bSTLDTbUkNsUI1avt0AoNECaWvdTr7dG6HZHqJJ+PosjHgHK0Xac/hP/oWuk0igWDjnQCHDSt1rkNg==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/with-simple-caching/-/with-simple-caching-0.9.5.tgz", + "integrity": "sha512-C99GmV+eHNuFoHIErEc5gKZ8QWVdx8KopvqjcP96rd6IaeKGp6iRy4R6sgRImdXaR8/K/hk6C2fyP1QKlgBrJw==", "requires": { "type-fns": "^0.4.1" } diff --git a/package.json b/package.json index de7df8f..ab331c3 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,6 @@ }, "dependencies": { "type-fns": "^0.4.1", - "with-simple-caching": "^0.9.4" + "with-simple-caching": "^0.9.5" } } diff --git a/src/createRemoteStateCachingContext.test.ts b/src/createRemoteStateCachingContext.test.ts index cd7581e..72c845d 100644 --- a/src/createRemoteStateCachingContext.test.ts +++ b/src/createRemoteStateCachingContext.test.ts @@ -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 ], }); @@ -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(), + 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 => { + 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 + }); + }); }); diff --git a/src/createRemoteStateCachingContext.ts b/src/createRemoteStateCachingContext.ts index 8a8a966..1fe2ca2 100644 --- a/src/createRemoteStateCachingContext.ts +++ b/src/createRemoteStateCachingContext.ts @@ -7,6 +7,7 @@ import { defaultValueDeserializationMethod, defaultKeySerializationMethod, KeySerializationMethod, + defaultValueSerializationMethod, } from 'with-simple-caching'; import { RemoteStateCacheContext, RemoteStateCacheContextQueryRegistration } from './RemoteStateCacheContext'; import { RemoteStateQueryInvalidationTrigger, RemoteStateQueryUpdateTrigger } from './RemoteStateQueryCachingOptions'; @@ -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 @@ -153,9 +154,24 @@ export const createRemoteStateCachingContext = < */ serialize?: { /** - * allow specifying a default serialization key + * allow specifying a default key serialization method */ - key: KeySerializationMethod; + key?: KeySerializationMethod; + + /** + * allow specifying a default value serialization method + */ + value?: (output: Promise) => SCV; + }; + + /** + * allow specifying default deserialization options + */ + deserialize?: { + /** + * allow specifying a default value deserialization method + */ + value?: (cached: SCV) => Promise; }; }) => { /** @@ -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 = any, CV extends any = ReturnType>( + const withRemoteStateQueryCaching = any, CV extends SCV = ReturnType>( logic: L, options: Omit, 'cache'> & WithRemoteStateCachingOptions, ): QueryWithRemoteStateCaching => { @@ -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['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 }); @@ -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 any>({ + const onMutationOutput = async
  • any>({ mutationName, mutationInput, mutationOutput, @@ -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,