mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Fix retry issue
This commit is contained in:
parent
7f2de7e1ec
commit
f4419c9fba
@ -3,15 +3,14 @@
|
||||
import { AsyncCache } from "@stackframe/stack-shared/dist/utils/caches";
|
||||
import { captureError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { getGlobal, setGlobal } from "@stackframe/stack-shared/dist/utils/globals";
|
||||
import { neverResolve } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
|
||||
import { Suspense, use, useEffect } from "react";
|
||||
import { Suspense, useEffect } from "react";
|
||||
|
||||
let isPrefetching = false;
|
||||
let isPrefetchingCounter = 0;
|
||||
let hasSetupHookPrefetcher = false;
|
||||
|
||||
type HookPrefetcherCallback = () => void;
|
||||
export type HookPrefetcherCallback = () => HookPrefetcherCallback[] | void;
|
||||
|
||||
export function HookPrefetcher(props: {
|
||||
callbacks: HookPrefetcherCallback[],
|
||||
@ -24,7 +23,7 @@ export function HookPrefetcher(props: {
|
||||
...(getGlobal("use-async-cache-execution-hooks") ?? []),
|
||||
(options: { caller: string, dependencies: any[], cache: AsyncCache<any, any> }) => {
|
||||
if (options.cache.isDirty(options.dependencies)) {
|
||||
if (isPrefetching) {
|
||||
if (isPrefetchingCounter > 0) {
|
||||
// all good, continue
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.info(`Prefetching ${options.caller}...`);
|
||||
@ -41,28 +40,37 @@ export function HookPrefetcher(props: {
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const components = props.callbacks.map((callback, i) => () => {
|
||||
isPrefetching = true;
|
||||
try {
|
||||
callback();
|
||||
return use(neverResolve());
|
||||
} finally {
|
||||
isPrefetching = false;
|
||||
}
|
||||
} );
|
||||
const PrefetchMany = (props: { callbacks: HookPrefetcherCallback[] }): React.ReactNode => {
|
||||
return <>
|
||||
{props.callbacks.map((callback, i) => {
|
||||
const Component = () => {
|
||||
isPrefetchingCounter++;
|
||||
try {
|
||||
const componentCallbacks = callback();
|
||||
if (componentCallbacks) {
|
||||
return <PrefetchMany callbacks={componentCallbacks} />;
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
isPrefetchingCounter--;
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
{components.map((Component, i) => (
|
||||
<ErrorBoundary
|
||||
key={i}
|
||||
errorComponent={HookPrefetcherErrorComponent}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</>;
|
||||
return (
|
||||
<ErrorBoundary
|
||||
key={i}
|
||||
errorComponent={HookPrefetcherErrorComponent}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
})}
|
||||
</>;
|
||||
};
|
||||
|
||||
return <PrefetchMany callbacks={props.callbacks} />;
|
||||
}
|
||||
|
||||
function HookPrefetcherErrorComponent(props: { error: Error }) {
|
||||
|
||||
@ -5,153 +5,230 @@ import { useUser } from "@stackframe/stack";
|
||||
import { previewTemplateSource } from "@stackframe/stack-shared/dist/helpers/emails";
|
||||
import { createCachedRegex } from "@stackframe/stack-shared/dist/utils/regex";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HookPrefetcher } from "./hook-prefetcher";
|
||||
import { HookPrefetcher, HookPrefetcherCallback } from "./hook-prefetcher";
|
||||
|
||||
// note that URL prefetchers are allowed to return early before execution of all hooks (but not call hook conditionally beyond that)
|
||||
// this is because we suspend the component
|
||||
const urlPrefetchers: Record<string, ((match: RegExpMatchArray, query: URLSearchParams, hash: string) => void)[]> = {
|
||||
const urlPrefetchers: Record<string, ((match: RegExpMatchArray, query: URLSearchParams, hash: string) => void | HookPrefetcherCallback[])[]> = {
|
||||
"/projects/*": [
|
||||
// TODO: we currently don't prefetch metrics as they are pretty slow to fetch
|
||||
// ([_, projectId]) => (useAdminApp(projectId) as any)[stackAppInternalsSymbol].useMetrics(false),
|
||||
],
|
||||
"/projects/*/**": [
|
||||
([_, projectId]) => useAdminApp(projectId).useProject().useConfig(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useProject().useConfig();
|
||||
},
|
||||
],
|
||||
"/projects/*/users": [
|
||||
// TODO: we currently don't prefetch metrics as they are pretty slow to fetch
|
||||
// ([_, projectId]) => (useAdminApp(projectId) as any)[stackAppInternalsSymbol].useMetrics(),
|
||||
([_, projectId]) => useAdminApp(projectId).useUsers({ limit: 1 }),
|
||||
([_, projectId]) => useAdminApp(projectId).useUsers({
|
||||
limit: 10,
|
||||
orderBy: "signedUpAt",
|
||||
desc: true,
|
||||
includeAnonymous: false,
|
||||
}),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useUsers({ limit: 1 });
|
||||
},
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useUsers({
|
||||
limit: 10,
|
||||
orderBy: "signedUpAt",
|
||||
desc: true,
|
||||
includeAnonymous: false,
|
||||
});
|
||||
},
|
||||
],
|
||||
"/projects/*/users/*": [
|
||||
([_, projectId, userId]) => useAdminApp(projectId).useUser(userId),
|
||||
([_, projectId, userId]) => {
|
||||
const user = useAdminApp(projectId).useUser(userId);
|
||||
user?.useContactChannels();
|
||||
},
|
||||
([_, projectId, userId]) => {
|
||||
const user = useAdminApp(projectId).useUser(userId);
|
||||
user?.useTeams();
|
||||
},
|
||||
([_, projectId, userId]) => {
|
||||
const user = useAdminApp(projectId).useUser(userId);
|
||||
user?.useOAuthProviders();
|
||||
if (user) {
|
||||
return [
|
||||
() => {
|
||||
user.useContactChannels();
|
||||
},
|
||||
() => {
|
||||
user.useTeams();
|
||||
},
|
||||
() => {
|
||||
user.useOAuthProviders();
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
],
|
||||
"/projects/*/team-settings": [
|
||||
([_, projectId]) => useAdminApp(projectId).useTeamPermissionDefinitions(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useTeamPermissionDefinitions();
|
||||
},
|
||||
],
|
||||
"/projects/*/team-permissions": [
|
||||
([_, projectId]) => useAdminApp(projectId).useTeamPermissionDefinitions(),
|
||||
([_, projectId]) => useAdminApp(projectId).useProjectPermissionDefinitions(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useTeamPermissionDefinitions();
|
||||
},
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useProjectPermissionDefinitions();
|
||||
},
|
||||
],
|
||||
"/projects/*/project-permissions": [
|
||||
([_, projectId]) => useAdminApp(projectId).useProjectPermissionDefinitions(),
|
||||
([_, projectId]) => useAdminApp(projectId).useTeamPermissionDefinitions(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useProjectPermissionDefinitions();
|
||||
},
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useTeamPermissionDefinitions();
|
||||
},
|
||||
],
|
||||
"/projects/*/teams": [
|
||||
([_, projectId]) => useAdminApp(projectId).useTeams(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useTeams();
|
||||
},
|
||||
],
|
||||
"/projects/*/teams/*": [
|
||||
([_, projectId]) => useAdminApp(projectId).useTeamPermissionDefinitions(),
|
||||
([_, projectId]) => useAdminApp(projectId).useUsers({ limit: 10 }),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useTeamPermissionDefinitions();
|
||||
},
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useUsers({ limit: 10 });
|
||||
},
|
||||
([_, projectId, teamId]) => {
|
||||
const team = useAdminApp(projectId).useTeam(teamId);
|
||||
team?.useUsers();
|
||||
if (team) {
|
||||
return [() => {
|
||||
team.useUsers();
|
||||
}];
|
||||
}
|
||||
},
|
||||
],
|
||||
"/projects/*/api-keys": [
|
||||
([_, projectId]) => useAdminApp(projectId).useInternalApiKeys(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useInternalApiKeys();
|
||||
},
|
||||
],
|
||||
"/projects/*/webhooks": [
|
||||
([_, projectId]) => useAdminApp(projectId).useSvixToken(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useSvixToken();
|
||||
},
|
||||
],
|
||||
"/projects/*/webhooks/*": [
|
||||
([_, projectId]) => useAdminApp(projectId).useSvixToken(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useSvixToken();
|
||||
},
|
||||
],
|
||||
"/projects/*/email-drafts": [
|
||||
([_, projectId]) => useAdminApp(projectId).useEmailDrafts(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useEmailDrafts();
|
||||
},
|
||||
],
|
||||
"/projects/*/email-drafts/*": [
|
||||
([_, projectId]) => useAdminApp(projectId).useEmailDrafts(),
|
||||
([_, projectId]) => useAdminApp(projectId).useEmailThemes(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useEmailDrafts();
|
||||
},
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useEmailThemes();
|
||||
},
|
||||
([_, projectId, draftId]) => {
|
||||
const adminApp = useAdminApp(projectId);
|
||||
const draft = adminApp.useEmailDrafts().find((d) => d.id === draftId);
|
||||
if (draft) {
|
||||
adminApp.useEmailPreview({
|
||||
themeId: draft.themeId,
|
||||
templateTsxSource: draft.tsxSource,
|
||||
});
|
||||
return [() => {
|
||||
adminApp.useEmailPreview({
|
||||
themeId: draft.themeId,
|
||||
templateTsxSource: draft.tsxSource,
|
||||
});
|
||||
}];
|
||||
}
|
||||
},
|
||||
],
|
||||
"/projects/*/emails": [
|
||||
([_, projectId]) => useAdminApp(projectId).useUsers({ limit: 10 }),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useUsers({ limit: 10 });
|
||||
},
|
||||
],
|
||||
"/projects/*/email-templates": [
|
||||
([_, projectId]) => useAdminApp(projectId).useEmailTemplates(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useEmailTemplates();
|
||||
},
|
||||
],
|
||||
"/projects/*/email-templates/*": [
|
||||
([_, projectId]) => useAdminApp(projectId).useEmailTemplates(),
|
||||
([_, projectId]) => useAdminApp(projectId).useEmailThemes(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useEmailTemplates();
|
||||
},
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useEmailThemes();
|
||||
},
|
||||
([_, projectId, templateId]) => {
|
||||
const adminApp = useAdminApp(projectId);
|
||||
const template = adminApp.useEmailTemplates().find((t) => t.id === templateId);
|
||||
if (template) {
|
||||
adminApp.useEmailPreview({
|
||||
themeId: template.themeId,
|
||||
templateTsxSource: template.tsxSource,
|
||||
});
|
||||
return [() => {
|
||||
adminApp.useEmailPreview({
|
||||
themeId: template.themeId,
|
||||
templateTsxSource: template.tsxSource,
|
||||
});
|
||||
}];
|
||||
}
|
||||
},
|
||||
],
|
||||
"/projects/*/email-themes": [
|
||||
([_, projectId]) => useAdminApp(projectId).useProject().useConfig(),
|
||||
([_, projectId]) => useAdminApp(projectId).useEmailThemes(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useProject().useConfig();
|
||||
},
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useEmailThemes();
|
||||
},
|
||||
([_, projectId]) => {
|
||||
const adminApp = useAdminApp(projectId);
|
||||
const themes = adminApp.useEmailThemes();
|
||||
themes.forEach((theme) => {
|
||||
adminApp.useEmailPreview({
|
||||
themeId: theme.id,
|
||||
templateTsxSource: previewTemplateSource,
|
||||
});
|
||||
return [() => {
|
||||
adminApp.useEmailPreview({
|
||||
themeId: theme.id,
|
||||
templateTsxSource: previewTemplateSource,
|
||||
});
|
||||
}];
|
||||
});
|
||||
},
|
||||
],
|
||||
"/projects/*/email-themes/*": [
|
||||
([_, projectId, themeId]) => useAdminApp(projectId).useEmailTheme(themeId),
|
||||
([_, projectId, themeId]) => {
|
||||
useAdminApp(projectId).useEmailTheme(themeId);
|
||||
},
|
||||
([_, projectId, themeId]) => {
|
||||
const adminApp = useAdminApp(projectId);
|
||||
const theme = adminApp.useEmailTheme(themeId);
|
||||
adminApp.useEmailPreview({
|
||||
themeTsxSource: theme.tsxSource,
|
||||
templateTsxSource: previewTemplateSource,
|
||||
});
|
||||
return [() => {
|
||||
adminApp.useEmailPreview({
|
||||
themeTsxSource: theme.tsxSource,
|
||||
templateTsxSource: previewTemplateSource,
|
||||
});
|
||||
}];
|
||||
},
|
||||
],
|
||||
"/projects/*/project-settings": [
|
||||
([_, projectId]) => useAdminApp(projectId).useProject(),
|
||||
([_, projectId]) => useAdminApp(projectId).useProject().useProductionModeErrors(),
|
||||
() => useUser({ or: "redirect", projectIdMustMatch: "internal" }),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useProject();
|
||||
},
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useProject().useProductionModeErrors();
|
||||
},
|
||||
() => {
|
||||
useUser({ or: "redirect", projectIdMustMatch: "internal" });
|
||||
},
|
||||
([_, projectId]) => {
|
||||
const project = useAdminApp(projectId).useProject();
|
||||
const teams = useUser({ or: "redirect", projectIdMustMatch: "internal" }).useTeams();
|
||||
const ownerTeam = teams.find((team) => team.id === project.ownerTeamId);
|
||||
ownerTeam?.useUsers();
|
||||
if (ownerTeam) {
|
||||
return [() => {
|
||||
ownerTeam.useUsers();
|
||||
}];
|
||||
}
|
||||
},
|
||||
],
|
||||
"/projects/*/payments/**": [
|
||||
([_, projectId]) => useAdminApp(projectId).useStripeAccountInfo(),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useStripeAccountInfo();
|
||||
},
|
||||
],
|
||||
"/projects/*/payments/transactions": [
|
||||
([_, projectId]) => useAdminApp(projectId).useTransactions({ limit: 10 }),
|
||||
([_, projectId]) => {
|
||||
useAdminApp(projectId).useTransactions({ limit: 10 });
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -45,6 +45,8 @@ export type ClientInterfaceOptions = {
|
||||
});
|
||||
|
||||
export class StackClientInterface {
|
||||
private pendingNetworkDiagnostics?: ReturnType<StackClientInterface["_runNetworkDiagnosticsInner"]>;
|
||||
|
||||
constructor(public readonly options: ClientInterfaceOptions) {
|
||||
// nothing here
|
||||
}
|
||||
@ -58,6 +60,19 @@ export class StackClientInterface {
|
||||
}
|
||||
|
||||
public async runNetworkDiagnostics(session?: InternalSession | null, requestType?: "client" | "server" | "admin") {
|
||||
if (this.pendingNetworkDiagnostics) {
|
||||
return await this.pendingNetworkDiagnostics;
|
||||
}
|
||||
|
||||
this.pendingNetworkDiagnostics = this._runNetworkDiagnosticsInner(session, requestType);
|
||||
try {
|
||||
return await this.pendingNetworkDiagnostics;
|
||||
} finally {
|
||||
this.pendingNetworkDiagnostics = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _runNetworkDiagnosticsInner(session?: InternalSession | null, requestType?: "client" | "server" | "admin") {
|
||||
const tryRequest = async (cb: () => Promise<void>) => {
|
||||
try {
|
||||
await cb();
|
||||
@ -72,12 +87,6 @@ export class StackClientInterface {
|
||||
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
|
||||
}
|
||||
});
|
||||
const apiRoot = session !== undefined && requestType !== undefined ? await tryRequest(async () => {
|
||||
const res = await this.sendClientRequestInner("/", {}, session!, requestType);
|
||||
if (res.status === "error") {
|
||||
throw res.error;
|
||||
}
|
||||
}) : "Not tested";
|
||||
const baseUrlBackend = await tryRequest(async () => {
|
||||
const res = await fetch(new URL("/health", this.getApiUrl()));
|
||||
if (!res.ok) {
|
||||
@ -99,7 +108,6 @@ export class StackClientInterface {
|
||||
return {
|
||||
"navigator?.onLine": globalVar.navigator?.onLine,
|
||||
cfTrace,
|
||||
apiRoot,
|
||||
baseUrlBackend,
|
||||
prodDashboard,
|
||||
prodBackend,
|
||||
|
||||
@ -102,7 +102,7 @@ export class AsyncCache<D extends any[], T> {
|
||||
}
|
||||
|
||||
async refreshWhere(predicate: (dependencies: D) => boolean) {
|
||||
const promises: Promise<T>[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const [dependencies, cache] of this._map) {
|
||||
if (predicate(dependencies)) {
|
||||
promises.push(cache.refresh());
|
||||
@ -111,6 +111,16 @@ export class AsyncCache<D extends any[], T> {
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async invalidateWhere(predicate: (dependencies: D) => boolean) {
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const [dependencies, cache] of this._map) {
|
||||
if (predicate(dependencies)) {
|
||||
promises.push(cache.invalidate().catch(() => undefined));
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
readonly isCacheAvailable = this._createKeyed("isCacheAvailable");
|
||||
readonly getIfCached = this._createKeyed("getIfCached");
|
||||
readonly getOrWait = this._createKeyed("getOrWait");
|
||||
@ -199,22 +209,21 @@ class AsyncValueCache<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refetches the value from the fetcher, and updates the cache with it.
|
||||
* If anyone is listening to the cache, refreshes the value, and sets it without invalidating the cache.
|
||||
*/
|
||||
async refresh(): Promise<T> {
|
||||
return await this.getOrWait("write-only");
|
||||
async refresh(): Promise<void> {
|
||||
if (this._subscriptionsCount > 0) {
|
||||
await this.getOrWait("write-only");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). If anyone was listening to it,
|
||||
* it will refresh immediately.
|
||||
* Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). It will refresh immediately.
|
||||
*/
|
||||
invalidate(): void {
|
||||
async invalidate(): Promise<void> {
|
||||
this._store.setUnavailable();
|
||||
this._pendingPromise = undefined;
|
||||
if (this._subscriptionsCount > 0) {
|
||||
runAsynchronously(this.refresh());
|
||||
}
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
@ -242,11 +251,11 @@ class AsyncValueCache<T> {
|
||||
if (--this._subscriptionsCount === 0) {
|
||||
const currentRefreshPromiseIndex = ++this._mostRecentRefreshPromiseIndex;
|
||||
runAsynchronously(async () => {
|
||||
// wait a few seconds; if anything changes during that time, we don't want to refresh
|
||||
// wait a few seconds; we want to keep the cache up during this time
|
||||
// else we do unnecessary requests if we unsubscribe and then subscribe again immediately
|
||||
await wait(5000);
|
||||
if (this._subscriptionsCount === 0 && currentRefreshPromiseIndex === this._mostRecentRefreshPromiseIndex) {
|
||||
this.invalidate();
|
||||
await this.invalidate();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user