From b77d127e3ad0d4f1194eaee2385bfd33d2952ce6 Mon Sep 17 00:00:00 2001 From: Ulad Kasach Date: Thu, 29 Dec 2022 08:29:07 -0500 Subject: [PATCH] fix(triggers): trigger updates and invaludations even on mutation error mutations may throw an error even after mutating the remote state, so allow users to trigger invalidations/updates even on error --- src/RemoteStateQueryCachingOptions.ts | 39 +++++++++++++++++++-- src/createRemoteStateCachingContext.test.ts | 15 ++++---- src/createRemoteStateCachingContext.ts | 20 ++++++++--- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/RemoteStateQueryCachingOptions.ts b/src/RemoteStateQueryCachingOptions.ts index d014185..fc2ca42 100644 --- a/src/RemoteStateQueryCachingOptions.ts +++ b/src/RemoteStateQueryCachingOptions.ts @@ -1,6 +1,18 @@ import { WithSimpleCachingOptions } from 'with-simple-caching'; import { MutationWithRemoteStateRegistration } from './createRemoteStateCachingContext'; +export enum MutationExecutionStatus { + /** + * the mutation successfully executed and resolved a value + */ + RESOLVED = 'RESOLVED', + + /** + * the mutation threw an error and rejected + */ + REJECTED = 'REJECTED', +} + /** * an invalidation trigger for the cache of a remote-state-query * - allows the user to specify which keys become invalid when a specific mutation fires @@ -24,8 +36,24 @@ export interface RemoteStateQueryInvalidationTrigger * - provides the list of all currently cached query strings */ affects: (args: { + /** + * the input the triggering mutation was invoked with + */ mutationInput: Parameters; - mutationOutput: ReturnType; + /** + * the output the triggering mutation produced + * + * note + * - this may be null if the mutation threw an error + */ + mutationOutput: ReturnType | null; + /** + * the status of the execution + */ + mutationStatus: MutationExecutionStatus; + /** + * specifies all of the keys that are currently cached + */ cachedQueryKeys: string[]; }) => { inputs?: Parameters[]; @@ -69,8 +97,15 @@ export interface RemoteStateQueryUpdateTrigger any, mutationInput: Parameters; /** * the output the triggering mutation produced + * + * note + * - this may be null if the mutation threw an error + */ + mutationOutput: ReturnType | null; + /** + * the status of the execution */ - mutationOutput: ReturnType; + mutationStatus: MutationExecutionStatus; }; }) => ReturnType; } diff --git a/src/createRemoteStateCachingContext.test.ts b/src/createRemoteStateCachingContext.test.ts index aa36cdd..a00ecfa 100644 --- a/src/createRemoteStateCachingContext.test.ts +++ b/src/createRemoteStateCachingContext.test.ts @@ -296,7 +296,7 @@ describe('createRemoteStateCachingContext', () => { expect(result6.length).toEqual(0); // should no longer have any results, since our updatedBy trigger should have removed the recipe by uuid expect(apiCalls.length).toEqual(3); // should not have had another api call, since we updated the cache, not invalidated it }); - it('should be not trigger invalidations nor updates if the mutation threw an error', async () => { + it('should still trigger invalidations and updates if the mutation threw an error', async () => { // start the context const { withRemoteStateQueryCaching, withRemoteStateMutationRegistration } = createRemoteStateCachingContext({ cache: createCache() }); @@ -377,10 +377,10 @@ describe('createRemoteStateCachingContext', () => { expect(error.message).toEqual('surprise!'); } - // prove that we did not invalidate the request, since the mutation failed + // prove that we invalidated the request const result5 = await queryGetRecipes.execute({ searchFor: 'steak' }); - expect(result5).toEqual(result1); // should have gotten the same result after cache invalidation - expect(apiCalls.length).toEqual(2); // and should not have called the api after cache invalidation + expect(result5).not.toEqual(result1); // should have gotten a different result after cache invalidation + expect(apiCalls.length).toEqual(3); // and should have called the api after cache invalidation // execute mutation to delete a recipe we've previously found for smoothie try { @@ -391,10 +391,11 @@ describe('createRemoteStateCachingContext', () => { expect(error.message).toEqual('surprise!'); } - // prove that we did not update the cached value for that request + // prove that we updated the cached value for that request const result6 = await queryGetRecipes.execute({ searchFor: 'smoothie' }); - expect(result6).toEqual(result2); - expect(apiCalls.length).toEqual(2); // should not have had another api call, since still would have been cached + expect(result2.length).toEqual(1); + expect(result6.length).toEqual(0); // should no longer have any results, since our updatedBy trigger should have removed the recipe by uuid + expect(apiCalls.length).toEqual(3); // should not have had another api call, since we updated the cache, not invalidated it }); }); diff --git a/src/createRemoteStateCachingContext.ts b/src/createRemoteStateCachingContext.ts index 2e7e185..80dedc3 100644 --- a/src/createRemoteStateCachingContext.ts +++ b/src/createRemoteStateCachingContext.ts @@ -8,7 +8,7 @@ import { WithSimpleCachingAsyncOptions, } from 'with-simple-caching'; import { RemoteStateCacheContext, RemoteStateCacheContextQueryRegistration } from './RemoteStateCacheContext'; -import { RemoteStateQueryInvalidationTrigger, RemoteStateQueryUpdateTrigger } from './RemoteStateQueryCachingOptions'; +import { MutationExecutionStatus, RemoteStateQueryInvalidationTrigger, RemoteStateQueryUpdateTrigger } from './RemoteStateQueryCachingOptions'; import { BadRequestError } from './errors/BadRequestError'; import { RemoteStateCache } from './RemoteStateCache'; import { defaultKeySerializationMethod, defaultValueDeserializationMethod, defaultValueSerializationMethod } from './defaults'; @@ -270,10 +270,12 @@ export const createRemoteStateCachingContext = < mutationName, mutationInput, mutationOutput, + mutationStatus, }: { mutationName: string; mutationInput: Parameters; - mutationOutput: ReturnType; + mutationOutput: ReturnType | null; + mutationStatus: MutationExecutionStatus; }) => { const registrations = Object.values(context.registered.queries); @@ -299,6 +301,7 @@ export const createRemoteStateCachingContext = < const invalidate = invalidatedByThisMutationDefinition.affects({ mutationInput, mutationOutput, + mutationStatus, cachedQueryKeys, }); @@ -325,6 +328,7 @@ export const createRemoteStateCachingContext = < const affected = updatedByThisMutationDefinition.affects({ mutationInput, mutationOutput, + mutationStatus, cachedQueryKeys, }); @@ -338,6 +342,7 @@ export const createRemoteStateCachingContext = < with: { mutationInput, mutationOutput, + mutationStatus, }, }) : undefined; @@ -364,9 +369,14 @@ export const createRemoteStateCachingContext = < // define the execute function, with triggers onMutationOutput const execute: L = (async (...args: Parameters): Promise> => { - const result = (await logic(...args)) as ReturnType; - await onMutationOutput({ mutationName, mutationInput: args, mutationOutput: result }); - return result; + try { + const result = (await logic(...args)) as ReturnType; + await onMutationOutput({ mutationName, mutationInput: args, mutationOutput: result, mutationStatus: MutationExecutionStatus.RESOLVED }); + return result; + } catch (error) { + await onMutationOutput({ mutationName, mutationInput: args, mutationOutput: null, mutationStatus: MutationExecutionStatus.REJECTED }); + throw error; + } }) as L; // return the extended logic