diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth/providers/page-client.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth/providers/page-client.tsx index 03172c237..664d7c311 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth/providers/page-client.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth/providers/page-client.tsx @@ -58,7 +58,7 @@ export default function ProvidersClient() { No auth providers enabled yet. Add one from the available providers below! ) : ( - + {oauthProviders.map((provider) => ( ))} diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/useAdminInterface.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/useAdminInterface.tsx index 395fa1c69..548cf2709 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/useAdminInterface.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/useAdminInterface.tsx @@ -2,24 +2,29 @@ import { StackAdminInterface } from "@stackframe/stack-shared"; import React from "react"; -import { useStrictMemo } from "@stackframe/stack-shared/src/hooks/use-strict-memo"; import { useUser } from "@stackframe/stack"; import { throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { cacheFunction } from "@stackframe/stack-shared/dist/utils/caches"; +import { CurrentUser } from "@stackframe/stack/dist/lib/stack-app"; const StackAdminInterfaceContext = React.createContext(null); +const createAdminInterface = cacheFunction((baseUrl: string, projectId: string, user: CurrentUser) => { + return new StackAdminInterface({ + baseUrl, + projectId, + internalAdminAccessToken: user.accessToken ?? throwErr("User must have an access token"), + }); +}); + export function AdminAppProvider(props: { projectId: string, children: React.ReactNode }) { const user = useUser({ or: "redirect" }); - const stackAdminApp = useStrictMemo(() => { - return new StackAdminInterface({ - baseUrl: process.env.NEXT_PUBLIC_STACK_URL || throwErr('missing NEXT_PUBLIC_STACK_URL environment variable'), - projectId: props.projectId, - - // TODO refresh the access token - internalAdminAccessToken: user.accessToken ?? throwErr("User must have an access token"), - }); - }, [props.projectId, user]); + const stackAdminApp = createAdminInterface( + process.env.NEXT_PUBLIC_STACK_URL || throwErr('missing NEXT_PUBLIC_STACK_URL environment variable'), + props.projectId, + user, + ); return ( diff --git a/packages/stack-shared/src/utils/caches.tsx b/packages/stack-shared/src/utils/caches.tsx index 4b07de4e8..c1fa57d8c 100644 --- a/packages/stack-shared/src/utils/caches.tsx +++ b/packages/stack-shared/src/utils/caches.tsx @@ -1,6 +1,25 @@ +import { DependenciesMap } from "./maps"; import { RateLimitOptions, ReactPromise, rateLimited } from "./promises"; import { AsyncStore, ReadonlyAsyncStore } from "./stores"; +/** + * Can be used to cache the result of a function call, for example for the `use` hook in React. + */ +export function cacheFunction(f: F): F { + const dependenciesMap = new DependenciesMap(); + + return ((...args: any) => { + if (dependenciesMap.has(args)) { + return dependenciesMap.get(args); + } + + const value = f(...args); + dependenciesMap.set(args, value); + return value; + }) as any as F; +} + + export class AsyncCache { private _map: WeakMap> = new Map(); diff --git a/packages/stack-shared/src/utils/maps.tsx b/packages/stack-shared/src/utils/maps.tsx new file mode 100644 index 000000000..ebfc021ae --- /dev/null +++ b/packages/stack-shared/src/utils/maps.tsx @@ -0,0 +1,126 @@ +import { Result } from "./results"; + +export class MaybeWeakMap { + private readonly _primitiveMap: Map; + private readonly _weakMap: WeakMap; + + constructor(entries?: readonly (readonly [K, V])[] | null) { + const entriesArray = [...entries ?? []]; + this._primitiveMap = new Map(entriesArray.filter((e) => !this._isAllowedInWeakMap(e[0]))); + this._weakMap = new WeakMap(entriesArray.filter((e): e is [K & WeakKey, V] => this._isAllowedInWeakMap(e[0]))); + } + + private _isAllowedInWeakMap(key: K): key is K & WeakKey { + return (typeof key === "object" && key !== null) || (typeof key === "symbol" && Symbol.keyFor(key) === undefined); + } + + get(key: K): V | undefined { + if (this._isAllowedInWeakMap(key)) { + return this._weakMap.get(key); + } else { + return this._primitiveMap.get(key); + } + } + + set(key: K, value: V): this { + if (this._isAllowedInWeakMap(key)) { + this._weakMap.set(key, value); + } else { + this._primitiveMap.set(key, value); + } + return this; + } + + delete(key: K): boolean { + if (this._isAllowedInWeakMap(key)) { + return this._weakMap.delete(key); + } else { + return this._primitiveMap.delete(key); + } + } + + has(key: K): boolean { + if (this._isAllowedInWeakMap(key)) { + return this._weakMap.has(key); + } else { + return this._primitiveMap.has(key); + } + } + + [Symbol.toStringTag] = "MaybeWeakMap"; +} + + +type DependenciesMapInner = ( + & { map: MaybeWeakMap> } + & ( + | { hasValue: true, value: V } + | { hasValue: false, value: undefined } + ) +); + +export class DependenciesMap { + private _inner: DependenciesMapInner = { map: new MaybeWeakMap(), hasValue: false, value: undefined }; + + private _valueToResult(inner: DependenciesMapInner): Result { + if (inner.hasValue) { + return Result.ok(inner.value); + } else { + return Result.error(undefined); + } + } + + + private _unwrapFromInner(dependencies: any[], inner: DependenciesMapInner): Result { + if ((dependencies.length === 0)) { + return this._valueToResult(inner); + } else { + const [key, ...rest] = dependencies; + const newInner = inner.map.get(key); + if (!newInner) { + return Result.error(undefined); + } + return this._unwrapFromInner(rest, newInner); + } + } + + private _setInInner(dependencies: any[], value: Result, inner: DependenciesMapInner): Result { + if (dependencies.length === 0) { + const res = this._valueToResult(inner); + if (value.status === "ok") { + inner.hasValue = true; + inner.value = value.data; + } else { + inner.hasValue = false; + inner.value = undefined; + } + return res; + } else { + const [key, ...rest] = dependencies; + let newInner = inner.map.get(key); + if (!newInner) { + inner.map.set(key, newInner = { map: new MaybeWeakMap(), hasValue: false, value: undefined }); + } + return this._setInInner(rest, value, newInner); + } + } + + get(dependencies: K): V | undefined { + return Result.or(this._unwrapFromInner(dependencies, this._inner), undefined); + } + + set(dependencies: K, value: V): this { + this._setInInner(dependencies, Result.ok(value), this._inner); + return this; + } + + delete(dependencies: K): boolean { + return this._setInInner(dependencies, Result.error(undefined), this._inner).status === "ok"; + } + + has(dependencies: K): boolean { + return this._unwrapFromInner(dependencies, this._inner).status === "ok"; + } + + [Symbol.toStringTag] = "DependenciesMap"; +} diff --git a/packages/stack/src/lib/cookie.ts b/packages/stack/src/lib/cookie.ts index b4a614c55..6fb99c090 100644 --- a/packages/stack/src/lib/cookie.ts +++ b/packages/stack/src/lib/cookie.ts @@ -1,14 +1,14 @@ import { generateRandomCodeVerifier, generateRandomState, calculatePKCECodeChallenge } from "oauth4webapi"; import Cookies from "js-cookie"; import { isClient } from "../utils/next"; -import { cookies } from '@stackframe/stack-sc'; +import { cookies as rscCookies } from '@stackframe/stack-sc'; export function getCookie(name: string): string | null { // TODO the differentiating factor should be RCC vs. RSC, not whether it's a client - if (isClient()) { - return Cookies.get(name) ?? null; + if (rscCookies) { + return rscCookies().get(name)?.value ?? null; } else { - return cookies().get(name)?.value ?? null; + return Cookies.get(name) ?? null; } } @@ -21,20 +21,18 @@ export function setOrDeleteCookie(name: string, value: string | null) { } export function deleteCookie(name: string) { - // TODO the differentiating factor should be RCC vs. RSC, not whether it's a client - if (isClient()) { - Cookies.remove(name); + if (rscCookies) { + rscCookies().delete(name); } else { - cookies().delete(name); + Cookies.remove(name); } } export function setCookie(name: string, value: string) { - // TODO the differentiating factor should be RCC vs. RSC, not whether it's a client - if (isClient()) { - Cookies.set(name, value); + if (rscCookies) { + rscCookies().set(name, value); } else { - cookies().set(name, value); + Cookies.set(name, value); } } diff --git a/packages/stack/src/providers/StackProviderClient.tsx b/packages/stack/src/providers/StackProviderClient.tsx index bfc72918a..a8ccc0840 100644 --- a/packages/stack/src/providers/StackProviderClient.tsx +++ b/packages/stack/src/providers/StackProviderClient.tsx @@ -1,9 +1,8 @@ "use client"; -import { cache, use } from "react"; +import { use } from "react"; import { StackClientApp, StackClientAppJson, stackAppInternalsSymbol } from "../lib/stack-app"; import React from "react"; -import { useStrictMemo } from "@stackframe/stack-shared/dist/hooks/use-strict-memo"; export const StackContext = React.createContext,