Source: react/hoc/WithCentralSyncStore.js

import hoistNonReactStatic from "hoist-non-react-statics";
import { forwardRef, useEffect, useId, useState, useCallback } from "@/import/react";
import { useMutation, useQueryClient } from "@/import/reactQuery";
import getStoreSyncHelper from "@/shared/helper/sync/store";
import CentralSyncHelper, { useAreCentralStoresInSyncWithServer, useAreThereCentralProblematicCommits } from "@/shared/helper/sync/central";
import { useToken } from "@/shared/helper/user/generic";
import useElectLeaderNode from "@/shared/hook/useElectLeaderNode";
import getStore from "@/shared/store/index";
import { GraphQLClient, gql } from "graphql-request";
import { isBrowser } from "@/shared/helper/general";
import getGenericFormHelper from "@/shared/helper/form/generic";
import * as Sentry from "@sentry/nextjs";
import ModalWithCloseButton from "../components/variant/modal/withCloseButton";

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || "Element";
}
const endpoint = process.env.NEXT_PUBLIC_GRAPHQL_URL;

const graphQLClientPOST = new GraphQLClient(endpoint, {
  method: "POST",
  jsonSerializer: {
    parse: JSON.parse,
    stringify: JSON.stringify,
  },
});
const updateHistory = gql`
  mutation StoreHistory($id: ID!, $coc: String!, $type: Type!, $changes: [[Int!]], $heads: [String!], $ttl: Int, $municipalityCode: String!) {
    storeHistory(input: { id: $id, coc: $coc, type: $type, changes: $changes, heads: $heads, ttl: $ttl, municipalityCode: $municipalityCode }) {
      changes
      heads
      status
      coc
      municipalityCode
      ttl
      category
    }
  }
`;
const createMeeting = gql`
  mutation CreateMeeting($coc: ID!, $municipalityCode: String!, $changes: [[Int]], $heads: [String]) {
    createMeeting(input: { coc: $coc, municipalityCode: $municipalityCode, changes: $changes, heads: $heads }) {
      changes
      heads
      status
      id
      municipalityCode
    }
  }
`;
const createInvestigation = gql`
  mutation CreateInvestigation($coc: ID!, $municipalityCode: String!, $changes: [[Int]], $heads: [String]) {
    createInvestigation(input: { coc: $coc, municipalityCode: $municipalityCode, changes: $changes, heads: $heads }) {
      changes
      heads
      status
      id
      municipalityCode
    }
  }
`;
const createTask = gql`
  mutation CreateTask($coc: ID!, $municipalityCode: String!, $changes: [[Int]], $heads: [String], $category: TaskCategory!, $originatesFrom: Meeting) {
    createTask(input: { coc: $coc, municipalityCode: $municipalityCode, changes: $changes, heads: $heads, category: $category, originatesFrom: $originatesFrom }) {
      changes
      heads
      status
      id
      municipalityCode
    }
  }
`;

const updateQuestionnaireHistory = gql`
  mutation StoreDeclarationHistory($id: ID!, $coc: String!, $municipalityCode: String!, $changes: [[Int!]], $heads: [String!]) {
    storeDeclarationHistory(input: { id: $id, coc: $coc, municipalityCode: $municipalityCode, changes: $changes, heads: $heads }) {
      changes
      heads
      status
    }
  }
`;
const updateQuestionnaireHistoryAndSign = gql`
  mutation StoreAndSignDeclarationHistory($id: ID!, $coc: String!, $municipalityCode: String!, $changes: [[Int!]], $heads: [String!]) {
    storeDeclarationHistory(input: { id: $id, coc: $coc, municipalityCode: $municipalityCode, changes: $changes, heads: $heads }) {
      changes
      heads
      status
    }
    signDeclaration(input: { id: $id, coc: $coc, municipalityCode: $municipalityCode, heads: $heads }) {
      status
      signature
      signedAt
    }
  }
`;

function StandaloneBackgroundSyncer(props) {
  // const areAllStoresInSync = useAreCentralStoresInSyncWithServer();
  const areThereProblematicCommits = useAreThereCentralProblematicCommits();
  const [open, setOpen] = useState(false);

  useEffect(() => {
    setOpen(areThereProblematicCommits);
  }, [areThereProblematicCommits]);

  const buttonCallback = useCallback(() => {
    if (typeof window !== "undefined") {
      window.location.reload(true);
    }
  }, []);

  // Future TODO's:
  // 1. stop exit page when still syncing (or ask)
  // 2. global status of sync level indication

  const queryClient = useQueryClient();

  const token = useToken();
  const hookId = useId(); // Unique consistent token according to React itself.
  const isElected = useElectLeaderNode(hookId, Symbol.for("CENTRAL_STORE"), Symbol.for("ALL_CENTRAL_COMMITS"));

  useEffect(() => {
    if (token) {
      graphQLClientPOST.setHeader("Authorization", token);
    }
  }, [token]);

  const { mutate: updateStore } = useMutation(["updateStore"], {
    mutationFn: async ({ changes, heads, municipalityCode, originatesFrom, category, coc, id, type, ttl, actions = [] }) => {
      if (typeof id !== "symbol" && type !== "questionnaire") {
        return graphQLClientPOST.request(updateHistory, {
          id,
          type,
          changes,
          heads,
          ttl,
          coc,
          municipalityCode,
        });
      }
      if (type === "meeting") {
        return graphQLClientPOST.request(createMeeting, {
          coc,
          municipalityCode,
          changes,
          heads,
          category,
        });
      }
      if (type === "investigation") {
        return graphQLClientPOST.request(createInvestigation, {
          coc,
          municipalityCode,
          changes,
          heads,
          category,
        });
      }
      if (type === "task") {
        return graphQLClientPOST.request(createTask, {
          coc,
          municipalityCode,
          changes,
          heads,
          category,
          originatesFrom,
        });
      }
      if (type === "questionnaire") {
        return graphQLClientPOST.request(actions.includes("sign") ? updateQuestionnaireHistoryAndSign : updateQuestionnaireHistory, {
          id,
          coc,
          municipalityCode,
          changes,
          heads,
        });
      }
    },
    onMutate: async ({ commitId, id, type }) => {
      // Cancel any outgoing refetches
      // Not that it's any big deal, but just a waste of bandwidth.
      await queryClient.cancelQueries({
        queryKey: ["getStoreHistory", type, id],
      });
      const StoreSyncHelper = getStoreSyncHelper(type, id, { createInstance: false })
      if (StoreSyncHelper) {
        StoreSyncHelper.updateCommitStatus(commitId, "SUBMITTING");
      }
      return { commitId, type, id, StoreSyncHelper };
    },
    onError: async (error, { municipalityCode, changes, heads, coc }, { commitId, StoreSyncHelper, id, type }) => {
      if (StoreSyncHelper) {
        StoreSyncHelper.updateCommitStatus(commitId, "ERROR");
      }
      let message = error.message;
      if (Array.isArray(error.response?.errors)) {
        message = error.response.errors.map(({ message }) => message).join(";");
      }

      Sentry.withScope(function (scope) {
        scope.setTag("municipality", municipalityCode || process.env.NEXT_PUBLIC_MUNICIPALITY_CODE || sessionStorage.getItem("municipalityCode"));
        scope.setContext("syncInfo", {
          localCommitId: commitId,
          type,
          id,
          changes,
          heads,
          coc,
          municipalityCode,
        })
        // setUser could be used here to set "accountManager at Veenendaal" or something :) will be handy in the future
        // group errors together based on the error message and resource type. Prevents spamming servers as well.
        scope.setFingerprint([message, type]);
        Sentry.captureException(error);
      });
    },
    onSuccess: async ({ createTask, createMeeting, createInvestigation, storeHistory, storeDeclarationHistory, signDeclaration } = {}, { id, type, commitId }) => {
      const StoreSyncHelper = getStoreSyncHelper(type, id);
      const createdObj = createTask ?? createMeeting ?? createInvestigation;
      let processObj = createdObj ?? storeHistory;
      processObj = processObj ?? storeDeclarationHistory;
      if (!processObj) {
        // We always expect an object back, something happened....
        StoreSyncHelper.updateCommitStatus(commitId, "ERROR");
        // console.log("ERROR with commit id", id);
        // TODO: send to sentry
        return;
      }
      StoreSyncHelper.updateCommitStatus(commitId, "COMPLETED");
      if (processObj?.heads) {
        StoreSyncHelper.syncedHeaders$ = processObj.heads;
        if (Array.isArray(processObj.changes) && processObj.changes.length > 0) {
          void getStore(type, id).applyChanges(processObj.changes);
        }
      }
      if (processObj?.ttl) {
        // TODO: for now only support setting a ttl, not removing it later on
        // void getStore(type, id).getStore().sendUpdateToStore({ type: "SET_TTL", value: processObj.ttl });
      }
      if (storeDeclarationHistory && signDeclaration) {
        // Certainty for the questionnaire where both functions are called.
        processObj = {
          ...storeDeclarationHistory,
          ...signDeclaration,
        };
      }
      const { status, declaredAt, signedAt, signature, category } = processObj;
      void getGenericFormHelper(type, id).updateInfo({ status, declaredAt, signedAt, signature, category });
    },
  });

  useEffect(() => {
    // Ensure that this specific function is only triggered once, no matter how many times the useFormStore hook is included within the app.
    if (!isElected) {
      return () => {};
    }
    const subscription = CentralSyncHelper.serverCommits$.subscribe(
      ({ commit: { heads, actions = [], changes }, id, metaData: { storeType, storeId, ...metaData } }) => {
        if (
          storeType === "questionnaire" &&
          isBrowser &&
          window.location.pathname === "/verklaring/verklaren" &&
          getStore(storeType, storeId).getSnapshot("declarationState.signed.input.value", { defaultValueOnMiss: false })
        ) {
          actions.push("sign");
        }
        updateStore({
          ...metaData,
          id: storeId,
          type: storeType,
          changes,
          heads,
          actions,
          commitId: id,
        });
      }
    );
    return () => {
      subscription.unsubscribe();
    };
  }, [updateStore, isElected]);

  if (areThereProblematicCommits) {
    return (
      <ModalWithCloseButton
        title="Er is zojuist een fout opgetreden"
        description="Mogelijk is de allerlaatste wijziging verloren gegaan. Herlaad de pagina om verder te gaan.
        Deze fout is automatisch gemeld bij het BasisBeeld-team. Onze excuses voor het ongemak."
        buttonText="Herlaad de pagina"
        open={open}
        setOpen={setOpen}
        buttonAction={buttonCallback}
        status="error"
      />
    );
  }

  return null;
}

/**
 * Add a modal showing "no internet connection" when the component is mounted and the user is not online
 * @param Component
 * @returns {function({readOnly: *, [p: string]: *}): *}
 */
function withCentralSyncStore(Component) {
  function WithCentralSyncStore(props, ref) {
    return (
      <>
        <StandaloneBackgroundSyncer />
        <Component {...props} ref={ref} />
      </>
    );
  }
  hoistNonReactStatic(WithCentralSyncStore, Component);
  WithCentralSyncStore.displayName = `WithCentralSyncStore(${getDisplayName(Component)})`;
  return forwardRef(WithCentralSyncStore);
}

export default withCentralSyncStore;