Skip to content

Instantly share code, notes, and snippets.

@xixixao
Created June 18, 2024 10:35
Show Gist options
  • Save xixixao/0f6afb0de62ed9124571593a41740a52 to your computer and use it in GitHub Desktop.
Save xixixao/0f6afb0de62ed9124571593a41740a52 to your computer and use it in GitHub Desktop.
Mock Convex React client wip
/* 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;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment