Created
June 18, 2024 10:35
-
-
Save xixixao/0f6afb0de62ed9124571593a41740a52 to your computer and use it in GitHub Desktop.
Mock Convex React client wip
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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