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();
},
});