Skip to content

Commit

Permalink
Add interceptor
Browse files Browse the repository at this point in the history
  • Loading branch information
kerrynf committed Mar 5, 2024
1 parent 39facf2 commit e8f57ba
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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);
Expand Down Expand Up @@ -110,6 +112,8 @@ describe("useNovaEventing", () => {
initialChildren,
);

expect(renderSpy).toHaveBeenCalledTimes(1);

wrapper.rerender(
<NovaEventingProvider
children={<TestComponent childrenText={updatedChildren} />}
Expand All @@ -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.", () => {
Expand All @@ -137,6 +142,7 @@ describe("useNovaEventing", () => {
expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe(
initialChildren,
);
expect(renderSpy).toHaveBeenCalledTimes(1);

wrapper.rerender(
<NovaEventingProvider
Expand All @@ -148,6 +154,7 @@ describe("useNovaEventing", () => {
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.
Expand All @@ -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();
Expand All @@ -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();
Expand Down
88 changes: 82 additions & 6 deletions packages/nova-react/src/eventing/nova-eventing-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<INovaEventingContext>({});

// 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 {
Expand Down Expand Up @@ -58,6 +59,14 @@ export interface NovaReactEventing {
generateEvent(eventWrapper: GeneratedEventWrapper): Promise<void>;
}

export interface InternalEventingContext {
eventingRef: React.MutableRefObject<NovaEventing>;
unmountEventingRef: React.MutableRefObject<NovaEventing>;
mapperRef: React.MutableRefObject<
(reactEventWrapper: ReactEventWrapper) => EventWrapper
>;
}

export const NovaEventingProvider: React.FunctionComponent<
NovaEventingProviderProps
> = ({ children, eventing, unmountEventing, reactEventMapper }) => {
Expand Down Expand Up @@ -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],
);

Expand All @@ -112,6 +121,56 @@ export const useNovaEventing = (): NovaReactEventing => {
return eventing;
};

interface NovaEventingInterceptorProps {
interceptor: (event: EventWrapper) => Promise<EventWrapper | undefined>;
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 (
<NovaEventingContext.Provider value={contextValue}>
{children}
</NovaEventingContext.Provider>
);
};
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
Expand All @@ -133,20 +192,37 @@ const generateEventing =
mapperRef: React.MutableRefObject<
(reactEventWrapper: ReactEventWrapper) => EventWrapper
>,
interceptorRef?: React.MutableRefObject<
(event: EventWrapper) => Promise<EventWrapper | undefined>
>,
) =>
(): 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: {
inputType: InputType.programmatic,
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();
},
});

0 comments on commit e8f57ba

Please sign in to comment.