From e8f57ba435ee2eeb35154683c56a1c8c820f2fb5 Mon Sep 17 00:00:00 2001 From: Kerryn Frampton Date: Tue, 5 Mar 2024 08:54:04 -0800 Subject: [PATCH] Add interceptor --- .../eventing/nova-eventing-provider.test.tsx | 9 ++ .../src/eventing/nova-eventing-provider.tsx | 88 +++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/packages/nova-react/src/eventing/nova-eventing-provider.test.tsx b/packages/nova-react/src/eventing/nova-eventing-provider.test.tsx index 89cc3df..b1c9381 100644 --- a/packages/nova-react/src/eventing/nova-eventing-provider.test.tsx +++ b/packages/nova-react/src/eventing/nova-eventing-provider.test.tsx @@ -28,6 +28,7 @@ describe("useNovaEventing", () => { let eventing: NovaEventing; let prevWrappedEventing: NovaReactEventing; let eventCallback: () => void; + const renderSpy = jest.fn(); const initialChildren = "initial children"; const updatedChildren = "updated children"; @@ -43,6 +44,7 @@ describe("useNovaEventing", () => { prevWrappedEventing = undefined as unknown as NovaReactEventing; TestComponent = ({ childrenText }) => { + renderSpy(); const wrappedEventing: NovaReactEventing = useNovaEventing(); expect(wrappedEventing).toBeDefined(); expect(wrappedEventing).not.toBe(eventing); @@ -110,6 +112,8 @@ describe("useNovaEventing", () => { initialChildren, ); +expect(renderSpy).toHaveBeenCalledTimes(1); + wrapper.rerender( } @@ -120,6 +124,7 @@ describe("useNovaEventing", () => { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( updatedChildren, ); + expect(renderSpy).toHaveBeenCalledTimes(2); }); test("Takes in children and eventing props, creates a stable wrapped NovaReactEventing instance from eventing across re-renders when children do not change.", () => { @@ -137,6 +142,7 @@ describe("useNovaEventing", () => { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( initialChildren, ); + expect(renderSpy).toHaveBeenCalledTimes(1); wrapper.rerender( { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( initialChildren, ); + expect(renderSpy).toHaveBeenCalledTimes(1); // Update eventing instance to test useRef pathway. This will ensure the wrapped eventing instance // returned from useEventing is stable from one render to the next. @@ -164,6 +171,7 @@ describe("useNovaEventing", () => { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( initialChildren, ); + expect(renderSpy).toHaveBeenCalledTimes(1); //Trigger a callback on the test child through eventing eventCallback(); @@ -186,6 +194,7 @@ describe("useNovaEventing", () => { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( initialChildren, ); + expect(renderSpy).toHaveBeenCalledTimes(1); //Trigger a callback on the test child through eventing eventCallback(); diff --git a/packages/nova-react/src/eventing/nova-eventing-provider.tsx b/packages/nova-react/src/eventing/nova-eventing-provider.tsx index 91d8e96..e6557d7 100644 --- a/packages/nova-react/src/eventing/nova-eventing-provider.tsx +++ b/packages/nova-react/src/eventing/nova-eventing-provider.tsx @@ -6,10 +6,11 @@ import invariant from "invariant"; // Context is initialized with an empty object and this is null-checked within the hooks const NovaEventingContext = React.createContext({}); -// Both properties are optional in the context for initialization state only, but eventing must be supplied in the props +// All properties are optional in the context for initialization state only, but eventing must be supplied in the props interface INovaEventingContext { eventing?: NovaReactEventing; unmountEventing?: NovaReactEventing; + internal?: InternalEventingContext; } interface NovaEventingProviderProps { @@ -58,6 +59,14 @@ export interface NovaReactEventing { generateEvent(eventWrapper: GeneratedEventWrapper): Promise; } +export interface InternalEventingContext { + eventingRef: React.MutableRefObject; + unmountEventingRef: React.MutableRefObject; + mapperRef: React.MutableRefObject< + (reactEventWrapper: ReactEventWrapper) => EventWrapper + >; +} + export const NovaEventingProvider: React.FunctionComponent< NovaEventingProviderProps > = ({ children, eventing, unmountEventing, reactEventMapper }) => { @@ -91,7 +100,7 @@ export const NovaEventingProvider: React.FunctionComponent< ); const contextValue = React.useMemo( - () => ({ eventing: reactEventing, unmountEventing: reactUnmountEventing }), + () => ({ eventing: reactEventing, unmountEventing: reactUnmountEventing, internal: { eventingRef, unmountEventingRef, mapperRef} }), [reactEventing, reactUnmountEventing], ); @@ -112,6 +121,56 @@ export const useNovaEventing = (): NovaReactEventing => { return eventing; }; +interface NovaEventingInterceptorProps { + interceptor: (event: EventWrapper) => Promise; + children?: React.ReactNode | undefined; +} + +export const NovaEventingInterceptor: React.FunctionComponent< +NovaEventingInterceptorProps +> = ({ children, interceptor }) => { +// Nova contexts provide a facade over framework functions +// We don't need to trigger rerender in children when we are rerendered +// or when the input functions change, we just need to make sure callbacks +// use the right functions +const interceptorRef = React.useRef(interceptor); +if (interceptorRef.current !== interceptor) { + interceptorRef.current = interceptor; +} + +const { internal } = React.useContext(NovaEventingContext); + +if (!internal) { + invariant( + internal, + "Nova Eventing provider must be initialized prior to creating NovaEventingInterceptor!", + ); +} + +const reactEventing = React.useMemo( + generateEventing(internal.eventingRef, internal.mapperRef, interceptorRef), + [], +); + +const reactUnmountEventing = React.useMemo( + generateEventing(internal.unmountEventingRef, internal.mapperRef, interceptorRef), + [], +); + +const contextValue = React.useMemo( + () => ({ eventing: reactEventing, unmountEventing: reactUnmountEventing, internal }), + [reactEventing, reactUnmountEventing], +); + +return ( + + {children} + +); +}; +NovaEventingInterceptor.displayName = "NovaEventingInterceptor"; + + /** * Used for eventing that should be triggered when the component is unmounted, such as within a useEffect cleanup function * If unmountEventing has not been supplied to the NovaEventingProvider, this will fallback to use the defualt eventing instance @@ -133,13 +192,23 @@ const generateEventing = mapperRef: React.MutableRefObject< (reactEventWrapper: ReactEventWrapper) => EventWrapper >, + interceptorRef?: React.MutableRefObject< + (event: EventWrapper) => Promise + >, ) => (): NovaReactEventing => ({ - bubble: (eventWrapper: ReactEventWrapper) => { + bubble: async (eventWrapper: ReactEventWrapper) => { const mappedEvent: EventWrapper = mapperRef.current(eventWrapper); - return eventingRef.current.bubble(mappedEvent); + if (!interceptorRef) { + return eventingRef.current.bubble(mappedEvent); + } + + let eventToBubble: EventWrapper | undefined = mappedEvent; + eventToBubble = await interceptorRef?.current(mappedEvent); + + return eventToBubble ? eventingRef.current.bubble(eventToBubble) : Promise.resolve(); }, - generateEvent: (eventWrapper: GeneratedEventWrapper) => { + generateEvent: async (eventWrapper: GeneratedEventWrapper) => { const mappedEvent = { event: eventWrapper.event, source: { @@ -147,6 +216,13 @@ const generateEventing = timeStamp: eventWrapper.timeStampOverride ?? Date.now(), }, }; - return eventingRef.current.bubble(mappedEvent); + if (!interceptorRef) { + return eventingRef.current.bubble(mappedEvent); + } + + let eventToBubble: EventWrapper | undefined = mappedEvent; + eventToBubble = await interceptorRef?.current(mappedEvent); + + return eventToBubble ? eventingRef.current.bubble(eventToBubble) : Promise.resolve(); }, });