Skip to content

Instantly share code, notes, and snippets.

@xixixao
Created June 18, 2024 10:35
Show Gist options
  • Select an option

  • Save xixixao/0f6afb0de62ed9124571593a41740a52 to your computer and use it in GitHub Desktop.

Select an option

Save xixixao/0f6afb0de62ed9124571593a41740a52 to your computer and use it in GitHub Desktop.

Revisions

  1. xixixao created this gist Jun 18, 2024.
    193 changes: 193 additions & 0 deletions mockConvexReactClient.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,193 @@
    /* eslint-disable class-methods-use-this */
    import { captureMessage } from "@sentry/nextjs";
    import { ConvexReactClient, Watch } from "convex/react";
    import { getFunctionName, FunctionReference } from "convex/server";
    import { ReactNode, useEffect } from "react";
    import { usePrevious } from "react-use";
    import { toast as sonnerToast } from "sonner";

    export async function copyTextToClipboard(text: string) {
    if ("clipboard" in navigator) {
    return navigator.clipboard.writeText(text);
    }
    return document.execCommand("copy", true, text);
    }

    export const isUserDefinedObject = (name: string) => !name.startsWith("_");

    /**
    * @param type What type of toast to render (decides which icon and colors to use).
    * @param message The message to display with the toast.
    * @param id If set, we will update the current toast if a toast with `id`
    * is already displayed instead of opening a new one.
    * @param duration The duration (in ms) before the toast is automatically close.
    * Use `false` to never auto-close this toast.
    */
    export function toast(
    type: "success" | "error" | "info",
    message: ReactNode,
    id?: string,
    duration?: number | false,
    ) {
    sonnerToast[type](message, {
    id,
    duration: duration !== false ? duration : Number.POSITIVE_INFINITY,
    });
    }

    export function dismissToast(id: string) {
    sonnerToast.dismiss(id);
    }

    export function mockConvexReactClient(): MockConvexReactClient &
    ConvexReactClient {
    return new MockConvexReactClient() as any;
    }

    class MockConvexReactClient {
    // These are written to and read from type safe APIs (`registerQueryFake`, `watchQuery`),
    // so it's ok to be looser with the types here since they're never directly accessed.
    private queries: Record<string, (...args: any[]) => any>;

    private mutations: Record<string, (...args: any[]) => any>;

    constructor() {
    this.queries = {};
    this.mutations = {};
    }

    registerQueryFake<FuncRef extends FunctionReference<"query", "public">>(
    funcRef: FuncRef,
    impl: (args: FuncRef["_args"]) => FuncRef["_returnType"],
    ): this {
    this.queries[getFunctionName(funcRef)] = impl;
    return this;
    }

    registerMutationFake<FuncRef extends FunctionReference<"mutation", "public">>(
    funcRef: FuncRef,
    impl: (args: FuncRef["_args"]) => FuncRef["_returnType"],
    ): this {
    this.mutations[getFunctionName(funcRef)] = impl;
    return this;
    }

    setAuth() {
    throw new Error("Auth is not implemented");
    }

    clearAuth() {
    throw new Error("Auth is not implemented");
    }

    watchQuery<Query extends FunctionReference<"query">>(
    query: FunctionReference<"query">,
    ...args: Query["_args"]
    ): Watch<Query["_returnType"]> {
    return {
    localQueryResult: () => {
    const name = getFunctionName(query);
    const queryImpl = this.queries && this.queries[name];
    if (queryImpl) {
    return queryImpl(...args);
    }
    throw new Error(
    `Unexpected query: ${name}. Try providing a function for this query in the mock client constructor.`,
    );
    },
    onUpdate: () => () => ({
    unsubscribe: () => null,
    }),
    journal: () => void 0,
    localQueryLogs: () => {
    throw new Error("not implemented");
    },
    };
    }

    mutation<Mutation extends FunctionReference<"mutation">>(
    mutation: Mutation,
    ...args: Mutation["_args"]
    ): Promise<Mutation["_returnType"]> {
    const name = getFunctionName(mutation);
    const mutationImpl = this.mutations && this.mutations[name];
    if (mutationImpl) {
    return mutationImpl(args[0]);
    }
    throw new Error(
    `Unexpected mutation: ${name}. Try providing a function for this mutation in the mock client constructor.`,
    );
    }

    action(): Promise<any> {
    throw new Error("Actions are not implemented");
    }

    connectionState() {
    return {
    hasInflightRequests: false,
    isWebSocketConnected: true,
    };
    }

    close() {
    return Promise.resolve();
    }
    }

    // utility for logging changed values in useEffect re-renders
    export const useEffectDebugger = (
    effectHook: Parameters<typeof useEffect>[0],
    dependencies: Parameters<typeof useEffect>[1],
    dependencyNames = [],
    ) => {
    const previousDeps = usePrevious(dependencies);

    const changedDeps =
    dependencies?.reduce(
    (acc: Record<string, { before: any; after: any }>, dependency, index) => {
    if (previousDeps && dependency !== previousDeps[index]) {
    const keyName = dependencyNames[index] || index;
    return {
    ...acc,
    [keyName]: {
    before: previousDeps[index],
    after: dependency,
    },
    };
    }

    return acc;
    },
    {},
    ) || {};

    if (Object.keys(changedDeps).length) {
    // eslint-disable-next-line no-console
    console.log("[useEffectDebugger] ", changedDeps);
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
    useEffect(effectHook, dependencies);
    };

    export const reportHttpError = (
    method: string,
    url: string,
    error: { code: string; message: string },
    ) => {
    captureMessage(
    `failed to request ${method} ${url}: ${error.code} - ${error.message} `,
    );
    };

    // Backoff numbers are in milliseconds.
    const INITIAL_BACKOFF = 500;
    const MAX_BACKOFF = 16000;

    export const backoffWithJitter = (numRetries: number) => {
    const baseBackoff = INITIAL_BACKOFF * 2 ** (numRetries - 1);
    const actualBackoff = Math.min(baseBackoff, MAX_BACKOFF);
    const jitter = actualBackoff * (Math.random() - 0.5);
    return actualBackoff + jitter;
    };