From 6fa4cbcb3c54018d283fb53738f6a0814643fdb9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Oct 2025 04:21:02 +0000 Subject: [PATCH 1/3] Refactor: Simplify signal creation and state proxy Introduces a factory function for creating resource signals and refactors the state proxy to use this factory. This reduces code duplication and improves maintainability. Co-authored-by: bryce --- packages/clerk-js/src/core/signals.ts | 93 +++-- packages/clerk-js/src/core/state.ts | 62 +-- packages/react/src/hooks/useClerkSignal.ts | 45 +-- packages/react/src/stateProxy.ts | 450 +++++++++++---------- packages/types/src/state.ts | 39 +- 5 files changed, 361 insertions(+), 328 deletions(-) diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index e3f98ad4787..b037be95e0a 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -7,47 +7,62 @@ import type { SignIn } from './resources/SignIn'; import type { SignUp } from './resources/SignUp'; import type { Waitlist } from './resources/Waitlist'; -export const signInResourceSignal = signal<{ resource: SignIn | null }>({ resource: null }); -export const signInErrorSignal = signal<{ error: unknown }>({ error: null }); -export const signInFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); - -export const signInComputedSignal: SignInSignal = computed(() => { - const signIn = signInResourceSignal().resource; - const error = signInErrorSignal().error; - const fetchStatus = signInFetchSignal().status; - - const errors = errorsToParsedErrors(error); - - return { errors, fetchStatus, signIn: signIn ? signIn.__internal_future : null }; -}); - -export const signUpResourceSignal = signal<{ resource: SignUp | null }>({ resource: null }); -export const signUpErrorSignal = signal<{ error: unknown }>({ error: null }); -export const signUpFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); - -export const signUpComputedSignal: SignUpSignal = computed(() => { - const signUp = signUpResourceSignal().resource; - const error = signUpErrorSignal().error; - const fetchStatus = signUpFetchSignal().status; - - const errors = errorsToParsedErrors(error); - - return { errors, fetchStatus, signUp: signUp ? signUp.__internal_future : null }; -}); - -export const waitlistResourceSignal = signal<{ resource: Waitlist | null }>({ resource: null }); -export const waitlistErrorSignal = signal<{ error: unknown }>({ error: null }); -export const waitlistFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); - -export const waitlistComputedSignal: WaitlistSignal = computed(() => { - const waitlist = waitlistResourceSignal().resource; - const error = waitlistErrorSignal().error; - const fetchStatus = waitlistFetchSignal().status; +interface ResourceSignalSet { + resourceSignal: ReturnType>; + errorSignal: ReturnType>; + fetchSignal: ReturnType>; + computedSignal: TComputedSignal; +} - const errors = errorsToParsedErrors(error); +function createResourceSignalSet< + TResource extends { __internal_future: any }, + TSignalName extends string, + TComputedSignal extends () => { errors: Errors; fetchStatus: 'idle' | 'fetching'; [K in TSignalName]: any }, +>( + resourceName: TSignalName, +): ResourceSignalSet { + const resourceSignal = signal<{ resource: TResource | null }>({ resource: null }); + const errorSignal = signal<{ error: unknown }>({ error: null }); + const fetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); + + const computedSignal = computed(() => { + const resource = resourceSignal().resource; + const error = errorSignal().error; + const fetchStatus = fetchSignal().status; + const errors = errorsToParsedErrors(error); + + return { + errors, + fetchStatus, + [resourceName]: resource ? resource.__internal_future : null, + } as ReturnType; + }) as TComputedSignal; + + return { + resourceSignal, + errorSignal, + fetchSignal, + computedSignal, + }; +} - return { errors, fetchStatus, waitlist: waitlist ? waitlist.__internal_future : null }; -}); +const signInSignals = createResourceSignalSet('signIn'); +export const signInResourceSignal = signInSignals.resourceSignal; +export const signInErrorSignal = signInSignals.errorSignal; +export const signInFetchSignal = signInSignals.fetchSignal; +export const signInComputedSignal = signInSignals.computedSignal; + +const signUpSignals = createResourceSignalSet('signUp'); +export const signUpResourceSignal = signUpSignals.resourceSignal; +export const signUpErrorSignal = signUpSignals.errorSignal; +export const signUpFetchSignal = signUpSignals.fetchSignal; +export const signUpComputedSignal = signUpSignals.computedSignal; + +const waitlistSignals = createResourceSignalSet('waitlist'); +export const waitlistResourceSignal = waitlistSignals.resourceSignal; +export const waitlistErrorSignal = waitlistSignals.errorSignal; +export const waitlistFetchSignal = waitlistSignals.fetchSignal; +export const waitlistComputedSignal = waitlistSignals.computedSignal; /** * Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index 1977947bf0e..dc8cb604a24 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -21,6 +21,13 @@ import { waitlistResourceSignal, } from './signals'; +type ResourceSignalSet = { + resourceSignal: ReturnType; + errorSignal: ReturnType; + fetchSignal: ReturnType; + computedSignal: ReturnType; +}; + export class State implements StateInterface { signInResourceSignal = signInResourceSignal; signInErrorSignal = signInErrorSignal; @@ -39,6 +46,15 @@ export class State implements StateInterface { private _waitlistInstance: Waitlist | null = null; + private readonly resourceSignalMap = new Map< + new (...args: any[]) => BaseResource, + ResourceSignalSet + >([ + [SignIn, { resourceSignal: signInResourceSignal, errorSignal: signInErrorSignal, fetchSignal: signInFetchSignal, computedSignal: signInComputedSignal }], + [SignUp, { resourceSignal: signUpResourceSignal, errorSignal: signUpErrorSignal, fetchSignal: signUpFetchSignal, computedSignal: signUpComputedSignal }], + [Waitlist, { resourceSignal: waitlistResourceSignal, errorSignal: waitlistErrorSignal, fetchSignal: waitlistFetchSignal, computedSignal: waitlistComputedSignal }], + ]); + __internal_effect = effect; __internal_computed = computed; @@ -55,45 +71,33 @@ export class State implements StateInterface { return this._waitlistInstance; } - private onResourceError = (payload: { resource: BaseResource; error: unknown }) => { - if (payload.resource instanceof SignIn) { - this.signInErrorSignal({ error: payload.error }); - } - - if (payload.resource instanceof SignUp) { - this.signUpErrorSignal({ error: payload.error }); + private getSignalSetForResource(resource: BaseResource): ResourceSignalSet | undefined { + for (const [ResourceClass, signalSet] of this.resourceSignalMap) { + if (resource instanceof ResourceClass) { + return signalSet; + } } + return undefined; + } - if (payload.resource instanceof Waitlist) { - this.waitlistErrorSignal({ error: payload.error }); + private onResourceError = (payload: { resource: BaseResource; error: unknown }) => { + const signalSet = this.getSignalSetForResource(payload.resource); + if (signalSet) { + signalSet.errorSignal({ error: payload.error }); } }; private onResourceUpdated = (payload: { resource: BaseResource }) => { - if (payload.resource instanceof SignIn) { - this.signInResourceSignal({ resource: payload.resource }); - } - - if (payload.resource instanceof SignUp) { - this.signUpResourceSignal({ resource: payload.resource }); - } - - if (payload.resource instanceof Waitlist) { - this.waitlistResourceSignal({ resource: payload.resource }); + const signalSet = this.getSignalSetForResource(payload.resource); + if (signalSet) { + signalSet.resourceSignal({ resource: payload.resource }); } }; private onResourceFetch = (payload: { resource: BaseResource; status: 'idle' | 'fetching' }) => { - if (payload.resource instanceof SignIn) { - this.signInFetchSignal({ status: payload.status }); - } - - if (payload.resource instanceof SignUp) { - this.signUpFetchSignal({ status: payload.status }); - } - - if (payload.resource instanceof Waitlist) { - this.waitlistFetchSignal({ status: payload.status }); + const signalSet = this.getSignalSetForResource(payload.resource); + if (signalSet) { + signalSet.fetchSignal({ status: payload.status }); } }; } diff --git a/packages/react/src/hooks/useClerkSignal.ts b/packages/react/src/hooks/useClerkSignal.ts index 617261f3932..ba4bbc12de9 100644 --- a/packages/react/src/hooks/useClerkSignal.ts +++ b/packages/react/src/hooks/useClerkSignal.ts @@ -1,17 +1,28 @@ import type { SignInSignalValue, SignUpSignalValue, WaitlistSignalValue } from '@clerk/types'; -import { useCallback, useSyncExternalStore } from 'react'; +import { useCallback, useMemo, useSyncExternalStore } from 'react'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider'; +type SignalName = 'signIn' | 'signUp' | 'waitlist'; + function useClerkSignal(signal: 'signIn'): SignInSignalValue; function useClerkSignal(signal: 'signUp'): SignUpSignalValue; function useClerkSignal(signal: 'waitlist'): WaitlistSignalValue; -function useClerkSignal(signal: 'signIn' | 'signUp' | 'waitlist'): SignInSignalValue | SignUpSignalValue | WaitlistSignalValue { +function useClerkSignal(signal: SignalName): SignInSignalValue | SignUpSignalValue | WaitlistSignalValue { useAssertWrappedByClerkProvider('useClerkSignal'); const clerk = useIsomorphicClerkContext(); + const signalGetter = useMemo(() => { + const map: Record SignInSignalValue | SignUpSignalValue | WaitlistSignalValue> = { + signIn: () => clerk.__internal_state.signInSignal() as SignInSignalValue, + signUp: () => clerk.__internal_state.signUpSignal() as SignUpSignalValue, + waitlist: () => clerk.__internal_state.waitlistSignal() as WaitlistSignalValue, + }; + return map[signal]; + }, [clerk.__internal_state, signal]); + const subscribe = useCallback( (callback: () => void) => { if (!clerk.loaded) { @@ -19,36 +30,16 @@ function useClerkSignal(signal: 'signIn' | 'signUp' | 'waitlist'): SignInSignalV } return clerk.__internal_state.__internal_effect(() => { - switch (signal) { - case 'signIn': - clerk.__internal_state.signInSignal(); - break; - case 'signUp': - clerk.__internal_state.signUpSignal(); - break; - case 'waitlist': - clerk.__internal_state.waitlistSignal(); - break; - default: - throw new Error(`Unknown signal: ${signal}`); - } + signalGetter(); callback(); }); }, - [clerk, clerk.loaded, clerk.__internal_state], + [clerk, clerk.loaded, clerk.__internal_state, signalGetter], ); + const getSnapshot = useCallback(() => { - switch (signal) { - case 'signIn': - return clerk.__internal_state.signInSignal() as SignInSignalValue; - case 'signUp': - return clerk.__internal_state.signUpSignal() as SignUpSignalValue; - case 'waitlist': - return clerk.__internal_state.waitlistSignal() as WaitlistSignalValue; - default: - throw new Error(`Unknown signal: ${signal}`); - } - }, [clerk.__internal_state]); + return signalGetter(); + }, [signalGetter]); const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index e7b4113d9d4..3b1fe2bac33 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -21,12 +21,204 @@ const defaultErrors = (): Errors => ({ global: null, }); +interface PropertyConfig { + [key: string]: { key: keyof TTarget; defaultValue: any }; +} + +interface MethodConfig { + [key: string]: keyof TTarget & string; +} + +interface StructConfig { + [key: string]: { + getTarget: () => TTarget; + methods: readonly (keyof TTarget & string)[]; + getters?: readonly (keyof TTarget)[]; + fallbacks?: Partial; + }; +} + +interface ResourceProxyConfig { + resourceName: TResourceName; + target: () => TTarget; + resourceProperties?: PropertyConfig; + resourceMethods?: MethodConfig; + resourceStructs?: StructConfig; + resourceDefaults?: Partial; +} + export class StateProxy implements State { constructor(private isomorphicClerk: IsomorphicClerk) {} - private readonly signInSignalProxy = this.buildSignInProxy(); - private readonly signUpSignalProxy = this.buildSignUpProxy(); - private readonly waitlistSignalProxy = this.buildWaitlistProxy(); + private readonly signInSignalProxy = this.createResourceProxy({ + resourceName: 'signIn', + target: () => this.client.signIn.__internal_future, + resourceProperties: { + id: { key: 'id', defaultValue: undefined }, + supportedFirstFactors: { key: 'supportedFirstFactors', defaultValue: [] }, + supportedSecondFactors: { key: 'supportedSecondFactors', defaultValue: [] }, + secondFactorVerification: { + key: 'secondFactorVerification', + defaultValue: { + status: null, + error: null, + expireAt: null, + externalVerificationRedirectURL: null, + nonce: null, + attempts: null, + message: null, + strategy: null, + verifiedAtClient: null, + verifiedFromTheSameClient: () => false, + __internal_toSnapshot: () => { + throw new Error('__internal_toSnapshot called before Clerk is loaded'); + }, + pathRoot: '', + reload: () => { + throw new Error('__internal_toSnapshot called before Clerk is loaded'); + }, + }, + }, + identifier: { key: 'identifier', defaultValue: null }, + createdSessionId: { key: 'createdSessionId', defaultValue: null }, + userData: { key: 'userData', defaultValue: {} }, + firstFactorVerification: { + key: 'firstFactorVerification', + defaultValue: { + status: null, + error: null, + expireAt: null, + externalVerificationRedirectURL: null, + nonce: null, + attempts: null, + message: null, + strategy: null, + verifiedAtClient: null, + verifiedFromTheSameClient: () => false, + __internal_toSnapshot: () => { + throw new Error('__internal_toSnapshot called before Clerk is loaded'); + }, + pathRoot: '', + reload: () => { + throw new Error('__internal_toSnapshot called before Clerk is loaded'); + }, + }, + }, + }, + resourceMethods: { + create: 'create', + password: 'password', + sso: 'sso', + finalize: 'finalize', + ticket: 'ticket', + passkey: 'passkey', + web3: 'web3', + }, + resourceStructs: { + emailCode: { + getTarget: () => this.client.signIn.__internal_future.emailCode, + methods: ['sendCode', 'verifyCode'] as const, + }, + emailLink: { + getTarget: () => this.client.signIn.__internal_future.emailLink, + methods: ['sendLink', 'waitForVerification'] as const, + getters: ['verification'] as const, + fallbacks: { verification: null }, + }, + resetPasswordEmailCode: { + getTarget: () => this.client.signIn.__internal_future.resetPasswordEmailCode, + methods: ['sendCode', 'verifyCode', 'submitPassword'] as const, + }, + phoneCode: { + getTarget: () => this.client.signIn.__internal_future.phoneCode, + methods: ['sendCode', 'verifyCode'] as const, + }, + mfa: { + getTarget: () => this.client.signIn.__internal_future.mfa, + methods: ['sendPhoneCode', 'verifyPhoneCode', 'verifyTOTP', 'verifyBackupCode'] as const, + }, + }, + resourceDefaults: { + status: 'needs_identifier', + availableStrategies: [], + isTransferable: false, + }, + }); + + private readonly signUpSignalProxy = this.createResourceProxy({ + resourceName: 'signUp', + target: () => this.client.signUp.__internal_future, + resourceProperties: { + id: { key: 'id', defaultValue: undefined }, + requiredFields: { key: 'requiredFields', defaultValue: [] }, + optionalFields: { key: 'optionalFields', defaultValue: [] }, + missingFields: { key: 'missingFields', defaultValue: [] }, + username: { key: 'username', defaultValue: null }, + firstName: { key: 'firstName', defaultValue: null }, + lastName: { key: 'lastName', defaultValue: null }, + emailAddress: { key: 'emailAddress', defaultValue: null }, + phoneNumber: { key: 'phoneNumber', defaultValue: null }, + web3Wallet: { key: 'web3Wallet', defaultValue: null }, + hasPassword: { key: 'hasPassword', defaultValue: false }, + unsafeMetadata: { key: 'unsafeMetadata', defaultValue: {} }, + createdSessionId: { key: 'createdSessionId', defaultValue: null }, + createdUserId: { key: 'createdUserId', defaultValue: null }, + abandonAt: { key: 'abandonAt', defaultValue: null }, + legalAcceptedAt: { key: 'legalAcceptedAt', defaultValue: null }, + locale: { key: 'locale', defaultValue: null }, + status: { key: 'status', defaultValue: 'missing_requirements' }, + unverifiedFields: { key: 'unverifiedFields', defaultValue: [] }, + isTransferable: { key: 'isTransferable', defaultValue: false }, + }, + resourceMethods: { + create: 'create', + update: 'update', + sso: 'sso', + password: 'password', + ticket: 'ticket', + web3: 'web3', + finalize: 'finalize', + }, + resourceStructs: { + verifications: { + getTarget: () => this.client.signUp.__internal_future.verifications, + methods: ['sendEmailCode', 'verifyEmailCode', 'sendPhoneCode', 'verifyPhoneCode'] as const, + }, + }, + }); + + private readonly waitlistSignalProxy = this.createResourceProxy({ + resourceName: 'waitlist', + target: (): { id?: string; createdAt: Date | null; updatedAt: Date | null; join: (params: any) => Promise } => { + if (!inBrowser() || !this.isomorphicClerk.loaded) { + return { + id: undefined, + createdAt: null, + updatedAt: null, + join: () => Promise.resolve({ error: null }), + }; + } + const state = this.isomorphicClerk.__internal_state; + const waitlist = state.__internal_waitlist; + if (waitlist && '__internal_future' in waitlist) { + return (waitlist as { __internal_future: any }).__internal_future; + } + return { + id: undefined, + createdAt: null, + updatedAt: null, + join: () => Promise.resolve({ error: null }), + }; + }, + resourceProperties: { + id: { key: 'id', defaultValue: undefined }, + createdAt: { key: 'createdAt', defaultValue: null }, + updatedAt: { key: 'updatedAt', defaultValue: null }, + }, + resourceMethods: { + join: 'join', + }, + }); signInSignal() { return this.signInSignalProxy; @@ -45,236 +237,52 @@ export class StateProxy implements State { return this.isomorphicClerk.__internal_state.__internal_waitlist; } - private buildSignInProxy() { - const gateProperty = this.gateProperty.bind(this); - const target = () => this.client.signIn.__internal_future; - - return { - errors: defaultErrors(), - fetchStatus: 'idle' as const, - signIn: { - status: 'needs_identifier' as const, - availableStrategies: [], - isTransferable: false, - get id() { - return gateProperty(target, 'id', undefined); - }, - get supportedFirstFactors() { - return gateProperty(target, 'supportedFirstFactors', []); - }, - get supportedSecondFactors() { - return gateProperty(target, 'supportedSecondFactors', []); - }, - get secondFactorVerification() { - return gateProperty(target, 'secondFactorVerification', { - status: null, - error: null, - expireAt: null, - externalVerificationRedirectURL: null, - nonce: null, - attempts: null, - message: null, - strategy: null, - verifiedAtClient: null, - verifiedFromTheSameClient: () => false, - __internal_toSnapshot: () => { - throw new Error('__internal_toSnapshot called before Clerk is loaded'); - }, - pathRoot: '', - reload: () => { - throw new Error('__internal_toSnapshot called before Clerk is loaded'); - }, - }); - }, - get identifier() { - return gateProperty(target, 'identifier', null); - }, - get createdSessionId() { - return gateProperty(target, 'createdSessionId', null); - }, - get userData() { - return gateProperty(target, 'userData', {}); - }, - get firstFactorVerification() { - return gateProperty(target, 'firstFactorVerification', { - status: null, - error: null, - expireAt: null, - externalVerificationRedirectURL: null, - nonce: null, - attempts: null, - message: null, - strategy: null, - verifiedAtClient: null, - verifiedFromTheSameClient: () => false, - __internal_toSnapshot: () => { - throw new Error('__internal_toSnapshot called before Clerk is loaded'); - }, - pathRoot: '', - reload: () => { - throw new Error('__internal_toSnapshot called before Clerk is loaded'); - }, - }); - }, - - create: this.gateMethod(target, 'create'), - password: this.gateMethod(target, 'password'), - sso: this.gateMethod(target, 'sso'), - finalize: this.gateMethod(target, 'finalize'), - - emailCode: this.wrapMethods(() => target().emailCode, ['sendCode', 'verifyCode'] as const), - emailLink: this.wrapStruct( - () => target().emailLink, - ['sendLink', 'waitForVerification'] as const, - ['verification'] as const, - { verification: null }, - ), - resetPasswordEmailCode: this.wrapMethods(() => target().resetPasswordEmailCode, [ - 'sendCode', - 'verifyCode', - 'submitPassword', - ] as const), - phoneCode: this.wrapMethods(() => target().phoneCode, ['sendCode', 'verifyCode'] as const), - mfa: this.wrapMethods(() => target().mfa, [ - 'sendPhoneCode', - 'verifyPhoneCode', - 'verifyTOTP', - 'verifyBackupCode', - ] as const), - ticket: this.gateMethod(target, 'ticket'), - passkey: this.gateMethod(target, 'passkey'), - web3: this.gateMethod(target, 'web3'), - }, - }; - } - - private buildSignUpProxy() { + private createResourceProxy( + config: ResourceProxyConfig, + ): { + errors: Errors; + fetchStatus: 'idle'; + [K in TResourceName]: any; + } { const gateProperty = this.gateProperty.bind(this); const gateMethod = this.gateMethod.bind(this); const wrapMethods = this.wrapMethods.bind(this); - const target = () => this.client.signUp.__internal_future; + const wrapStruct = this.wrapStruct.bind(this); + const target = config.target; - return { - errors: defaultErrors(), - fetchStatus: 'idle' as const, - signUp: { - get id() { - return gateProperty(target, 'id', undefined); - }, - get requiredFields() { - return gateProperty(target, 'requiredFields', []); - }, - get optionalFields() { - return gateProperty(target, 'optionalFields', []); - }, - get missingFields() { - return gateProperty(target, 'missingFields', []); - }, - get username() { - return gateProperty(target, 'username', null); - }, - get firstName() { - return gateProperty(target, 'firstName', null); - }, - get lastName() { - return gateProperty(target, 'lastName', null); - }, - get emailAddress() { - return gateProperty(target, 'emailAddress', null); - }, - get phoneNumber() { - return gateProperty(target, 'phoneNumber', null); - }, - get web3Wallet() { - return gateProperty(target, 'web3Wallet', null); - }, - get hasPassword() { - return gateProperty(target, 'hasPassword', false); - }, - get unsafeMetadata() { - return gateProperty(target, 'unsafeMetadata', {}); - }, - get createdSessionId() { - return gateProperty(target, 'createdSessionId', null); - }, - get createdUserId() { - return gateProperty(target, 'createdUserId', null); - }, - get abandonAt() { - return gateProperty(target, 'abandonAt', null); - }, - get legalAcceptedAt() { - return gateProperty(target, 'legalAcceptedAt', null); - }, - get locale() { - return gateProperty(target, 'locale', null); - }, - get status() { - return gateProperty(target, 'status', 'missing_requirements'); - }, - get unverifiedFields() { - return gateProperty(target, 'unverifiedFields', []); - }, - get isTransferable() { - return gateProperty(target, 'isTransferable', false); - }, - - create: gateMethod(target, 'create'), - update: gateMethod(target, 'update'), - sso: gateMethod(target, 'sso'), - password: gateMethod(target, 'password'), - ticket: gateMethod(target, 'ticket'), - web3: gateMethod(target, 'web3'), - finalize: gateMethod(target, 'finalize'), + const resource: any = { ...config.resourceDefaults }; - verifications: wrapMethods(() => target().verifications, [ - 'sendEmailCode', - 'verifyEmailCode', - 'sendPhoneCode', - 'verifyPhoneCode', - ] as const), - }, - }; - } + if (config.resourceProperties) { + for (const [propName, { key, defaultValue }] of Object.entries(config.resourceProperties)) { + Object.defineProperty(resource, propName, { + get: () => gateProperty(target, key as keyof TTarget, defaultValue), + enumerable: true, + }); + } + } - private buildWaitlistProxy() { - const gateProperty = this.gateProperty.bind(this); - const gateMethod = this.gateMethod.bind(this); - const fallbackWaitlistFuture = { - id: undefined, - createdAt: null, - updatedAt: null, - join: () => Promise.resolve({ error: null }), - }; - const target = (): typeof fallbackWaitlistFuture => { - if (!inBrowser() || !this.isomorphicClerk.loaded) { - return fallbackWaitlistFuture; + if (config.resourceMethods) { + for (const [methodName, methodKey] of Object.entries(config.resourceMethods)) { + resource[methodName] = gateMethod(target, methodKey); } - const state = this.isomorphicClerk.__internal_state; - const waitlist = state.__internal_waitlist; - if (waitlist && '__internal_future' in waitlist) { - return (waitlist as { __internal_future: typeof fallbackWaitlistFuture }).__internal_future; + } + + if (config.resourceStructs) { + for (const [structName, structConfig] of Object.entries(config.resourceStructs)) { + resource[structName] = wrapStruct( + structConfig.getTarget, + structConfig.methods, + structConfig.getters || [], + structConfig.fallbacks || {}, + ); } - return fallbackWaitlistFuture; - }; + } return { errors: defaultErrors(), fetchStatus: 'idle' as const, - waitlist: { - get id() { - return gateProperty(target, 'id', undefined); - }, - get createdAt() { - return gateProperty(target, 'createdAt', null); - }, - get updatedAt() { - return gateProperty(target, 'updatedAt', null); - }, - - join: gateMethod(target, 'join'), - }, - }; + [config.resourceName]: resource, + } as any; } __internal_effect(_: () => void): () => void { diff --git a/packages/types/src/state.ts b/packages/types/src/state.ts index 457d2cef7aa..4163be004e6 100644 --- a/packages/types/src/state.ts +++ b/packages/types/src/state.ts @@ -84,10 +84,24 @@ export interface Errors { global: unknown[] | null; // does not include any errors that could be parsed as a field error } +type ResourceSignalValue = { + errors: Errors; + fetchStatus: 'idle' | 'fetching'; + [K in TSignalName]: TFutureResource; +}; + +type ResourceSignal = () => Omit< + ResourceSignalValue, + TSignalName +> & { + [K in TSignalName]: TFutureResource | null; +}; + /** * The value returned by the `useSignInSignal` hook. */ -export interface SignInSignalValue { +export interface SignInSignalValue + extends ResourceSignalValue { /** * Represents the errors that occurred during the last fetch of the parent resource. */ @@ -104,11 +118,12 @@ export interface SignInSignalValue { export type NullableSignInSignal = Omit & { signIn: SignInFutureResource | null; }; -export interface SignInSignal { - (): NullableSignInSignal; -} +export interface SignInSignal extends ResourceSignal {} -export interface SignUpSignalValue { +/** + * The value returned by the `useSignUpSignal` hook. + */ +export interface SignUpSignalValue extends ResourceSignalValue { /** * The errors that occurred during the last fetch of the underlying `SignUp` resource. */ @@ -125,11 +140,13 @@ export interface SignUpSignalValue { export type NullableSignUpSignal = Omit & { signUp: SignUpFutureResource | null; }; -export interface SignUpSignal { - (): NullableSignUpSignal; -} +export interface SignUpSignal extends ResourceSignal {} -export interface WaitlistSignalValue { +/** + * The value returned by the `useWaitlistSignal` hook. + */ +export interface WaitlistSignalValue + extends ResourceSignalValue { /** * The errors that occurred during the last fetch of the underlying `Waitlist` resource. */ @@ -146,9 +163,7 @@ export interface WaitlistSignalValue { export type NullableWaitlistSignal = Omit & { waitlist: WaitlistFutureResource | null; }; -export interface WaitlistSignal { - (): NullableWaitlistSignal; -} +export interface WaitlistSignal extends ResourceSignal {} export interface State { /** From 4742887d855db920029d04992db8a6e007b340cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Oct 2025 04:23:59 +0000 Subject: [PATCH 2/3] Refactor: Centralize resource signal management Introduce a registry for resource signals and a helper function to retrieve them. This simplifies signal management and makes it easier to add new resources. Co-authored-by: bryce --- .../clerk-js/src/core/resources/SignIn.ts | 1 + .../clerk-js/src/core/resources/SignUp.ts | 1 + .../clerk-js/src/core/resources/Waitlist.ts | 1 + packages/clerk-js/src/core/signals.ts | 52 ++++++++++-- packages/clerk-js/src/core/state.ts | 80 +++++++++---------- 5 files changed, 89 insertions(+), 46 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index e98538e1ba5..88892ff381c 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -96,6 +96,7 @@ import { eventBus } from '../events'; import { BaseResource, UserData, Verification } from './internal'; export class SignIn extends BaseResource implements SignInResource { + static readonly __internal_resourceName = 'signIn' as const; pathRoot = '/client/sign_ins'; id?: string; diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index fbc697a210f..2d81fcb515b 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -76,6 +76,7 @@ declare global { } export class SignUp extends BaseResource implements SignUpResource { + static readonly __internal_resourceName = 'signUp' as const; pathRoot = '/client/sign_ups'; id: string | undefined; diff --git a/packages/clerk-js/src/core/resources/Waitlist.ts b/packages/clerk-js/src/core/resources/Waitlist.ts index 044f3888061..f5ca4d820b5 100644 --- a/packages/clerk-js/src/core/resources/Waitlist.ts +++ b/packages/clerk-js/src/core/resources/Waitlist.ts @@ -6,6 +6,7 @@ import { eventBus } from '../events'; import { BaseResource } from './internal'; export class Waitlist extends BaseResource implements WaitlistResource { + static readonly __internal_resourceName = 'waitlist' as const; pathRoot = '/waitlist'; id = ''; diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index b037be95e0a..9d0536e5817 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -3,9 +3,10 @@ import { snakeToCamel } from '@clerk/shared/underscore'; import type { Errors, SignInSignal, SignUpSignal, WaitlistSignal } from '@clerk/types'; import { computed, signal } from 'alien-signals'; -import type { SignIn } from './resources/SignIn'; -import type { SignUp } from './resources/SignUp'; -import type { Waitlist } from './resources/Waitlist'; +import { SignIn } from './resources/SignIn'; +import { SignUp } from './resources/SignUp'; +import { Waitlist } from './resources/Waitlist'; +import type { BaseResource } from './resources/Base'; interface ResourceSignalSet { resourceSignal: ReturnType>; @@ -14,6 +15,13 @@ interface ResourceSignalSet = new (...args: any[]) => T & { + __internal_future: any; + static __internal_resourceName: string; +}; + +type ResourceName = T['__internal_resourceName']; + function createResourceSignalSet< TResource extends { __internal_future: any }, TSignalName extends string, @@ -46,24 +54,56 @@ function createResourceSignalSet< }; } -const signInSignals = createResourceSignalSet('signIn'); +const resourceSignalRegistry = new Map< + ResourceClass, + ResourceSignalSet +>(); + +function registerResourceSignals< + TResourceClass extends ResourceClass, + TResource extends InstanceType, + TSignalName extends ResourceName, +>( + ResourceClass: TResourceClass & { __internal_resourceName: TSignalName }, + signalType: () => { errors: Errors; fetchStatus: 'idle' | 'fetching'; [K in TSignalName]: any }, +): ResourceSignalSet> { + const resourceName = ResourceClass.__internal_resourceName; + const signalSet = createResourceSignalSet>( + resourceName, + ); + resourceSignalRegistry.set(ResourceClass as ResourceClass, signalSet); + return signalSet; +} + +const signInSignals = registerResourceSignals(SignIn, (() => ({})) as SignInSignal); export const signInResourceSignal = signInSignals.resourceSignal; export const signInErrorSignal = signInSignals.errorSignal; export const signInFetchSignal = signInSignals.fetchSignal; export const signInComputedSignal = signInSignals.computedSignal; -const signUpSignals = createResourceSignalSet('signUp'); +const signUpSignals = registerResourceSignals(SignUp, (() => ({})) as SignUpSignal); export const signUpResourceSignal = signUpSignals.resourceSignal; export const signUpErrorSignal = signUpSignals.errorSignal; export const signUpFetchSignal = signUpSignals.fetchSignal; export const signUpComputedSignal = signUpSignals.computedSignal; -const waitlistSignals = createResourceSignalSet('waitlist'); +const waitlistSignals = registerResourceSignals(Waitlist, (() => ({})) as WaitlistSignal); export const waitlistResourceSignal = waitlistSignals.resourceSignal; export const waitlistErrorSignal = waitlistSignals.errorSignal; export const waitlistFetchSignal = waitlistSignals.fetchSignal; export const waitlistComputedSignal = waitlistSignals.computedSignal; +export function getResourceSignalSet( + resource: BaseResource, +): ResourceSignalSet | undefined { + for (const [ResourceClass, signalSet] of resourceSignalRegistry) { + if (resource instanceof ResourceClass) { + return signalSet; + } + } + return undefined; +} + /** * Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put * generic non-API errors into the global array. diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index dc8cb604a24..2ac83657ac9 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -7,6 +7,7 @@ import { SignIn } from './resources/SignIn'; import { SignUp } from './resources/SignUp'; import { Waitlist } from './resources/Waitlist'; import { + getResourceSignalSet, signInComputedSignal, signInErrorSignal, signInFetchSignal, @@ -21,40 +22,48 @@ import { waitlistResourceSignal, } from './signals'; -type ResourceSignalSet = { - resourceSignal: ReturnType; - errorSignal: ReturnType; - fetchSignal: ReturnType; - computedSignal: ReturnType; -}; - export class State implements StateInterface { - signInResourceSignal = signInResourceSignal; - signInErrorSignal = signInErrorSignal; - signInFetchSignal = signInFetchSignal; - signInSignal = signInComputedSignal; + get signInResourceSignal() { + return signInResourceSignal; + } + get signInErrorSignal() { + return signInErrorSignal; + } + get signInFetchSignal() { + return signInFetchSignal; + } + get signInSignal() { + return signInComputedSignal; + } - signUpResourceSignal = signUpResourceSignal; - signUpErrorSignal = signUpErrorSignal; - signUpFetchSignal = signUpFetchSignal; - signUpSignal = signUpComputedSignal; + get signUpResourceSignal() { + return signUpResourceSignal; + } + get signUpErrorSignal() { + return signUpErrorSignal; + } + get signUpFetchSignal() { + return signUpFetchSignal; + } + get signUpSignal() { + return signUpComputedSignal; + } - waitlistResourceSignal = waitlistResourceSignal; - waitlistErrorSignal = waitlistErrorSignal; - waitlistFetchSignal = waitlistFetchSignal; - waitlistSignal = waitlistComputedSignal; + get waitlistResourceSignal() { + return waitlistResourceSignal; + } + get waitlistErrorSignal() { + return waitlistErrorSignal; + } + get waitlistFetchSignal() { + return waitlistFetchSignal; + } + get waitlistSignal() { + return waitlistComputedSignal; + } private _waitlistInstance: Waitlist | null = null; - private readonly resourceSignalMap = new Map< - new (...args: any[]) => BaseResource, - ResourceSignalSet - >([ - [SignIn, { resourceSignal: signInResourceSignal, errorSignal: signInErrorSignal, fetchSignal: signInFetchSignal, computedSignal: signInComputedSignal }], - [SignUp, { resourceSignal: signUpResourceSignal, errorSignal: signUpErrorSignal, fetchSignal: signUpFetchSignal, computedSignal: signUpComputedSignal }], - [Waitlist, { resourceSignal: waitlistResourceSignal, errorSignal: waitlistErrorSignal, fetchSignal: waitlistFetchSignal, computedSignal: waitlistComputedSignal }], - ]); - __internal_effect = effect; __internal_computed = computed; @@ -71,31 +80,22 @@ export class State implements StateInterface { return this._waitlistInstance; } - private getSignalSetForResource(resource: BaseResource): ResourceSignalSet | undefined { - for (const [ResourceClass, signalSet] of this.resourceSignalMap) { - if (resource instanceof ResourceClass) { - return signalSet; - } - } - return undefined; - } - private onResourceError = (payload: { resource: BaseResource; error: unknown }) => { - const signalSet = this.getSignalSetForResource(payload.resource); + const signalSet = getResourceSignalSet(payload.resource); if (signalSet) { signalSet.errorSignal({ error: payload.error }); } }; private onResourceUpdated = (payload: { resource: BaseResource }) => { - const signalSet = this.getSignalSetForResource(payload.resource); + const signalSet = getResourceSignalSet(payload.resource); if (signalSet) { signalSet.resourceSignal({ resource: payload.resource }); } }; private onResourceFetch = (payload: { resource: BaseResource; status: 'idle' | 'fetching' }) => { - const signalSet = this.getSignalSetForResource(payload.resource); + const signalSet = getResourceSignalSet(payload.resource); if (signalSet) { signalSet.fetchSignal({ status: payload.status }); } From 9e948078c3cb7c040d67653ce4842799b3b1a046 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Oct 2025 04:25:33 +0000 Subject: [PATCH 3/3] Refactor: Centralize signal retrieval by resource name This change introduces a map to store and retrieve signal sets by resource name, simplifying signal access and removing redundant signal getters from the State class. Co-authored-by: bryce --- packages/clerk-js/src/core/signals.ts | 9 +++ packages/clerk-js/src/core/state.ts | 93 +++++++++------------- packages/react/src/hooks/useClerkSignal.ts | 11 ++- 3 files changed, 53 insertions(+), 60 deletions(-) diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index 9d0536e5817..ce23d258616 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -59,6 +59,8 @@ const resourceSignalRegistry = new Map< ResourceSignalSet >(); +const resourceNameToSignalSet = new Map>(); + function registerResourceSignals< TResourceClass extends ResourceClass, TResource extends InstanceType, @@ -72,9 +74,16 @@ function registerResourceSignals< resourceName, ); resourceSignalRegistry.set(ResourceClass as ResourceClass, signalSet); + resourceNameToSignalSet.set(resourceName, signalSet); return signalSet; } +export function getSignalSetByResourceName( + resourceName: string, +): ResourceSignalSet | undefined { + return resourceNameToSignalSet.get(resourceName); +} + const signInSignals = registerResourceSignals(SignIn, (() => ({})) as SignInSignal); export const signInResourceSignal = signInSignals.resourceSignal; export const signInErrorSignal = signInSignals.errorSignal; diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index 2ac83657ac9..bea45d72a92 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -3,65 +3,18 @@ import { computed, effect } from 'alien-signals'; import { eventBus } from './events'; import type { BaseResource } from './resources/Base'; -import { SignIn } from './resources/SignIn'; -import { SignUp } from './resources/SignUp'; import { Waitlist } from './resources/Waitlist'; import { getResourceSignalSet, - signInComputedSignal, - signInErrorSignal, - signInFetchSignal, - signInResourceSignal, - signUpComputedSignal, - signUpErrorSignal, - signUpFetchSignal, - signUpResourceSignal, - waitlistComputedSignal, - waitlistErrorSignal, - waitlistFetchSignal, - waitlistResourceSignal, + getSignalSetByResourceName, } from './signals'; -export class State implements StateInterface { - get signInResourceSignal() { - return signInResourceSignal; - } - get signInErrorSignal() { - return signInErrorSignal; - } - get signInFetchSignal() { - return signInFetchSignal; - } - get signInSignal() { - return signInComputedSignal; - } - - get signUpResourceSignal() { - return signUpResourceSignal; - } - get signUpErrorSignal() { - return signUpErrorSignal; - } - get signUpFetchSignal() { - return signUpFetchSignal; - } - get signUpSignal() { - return signUpComputedSignal; - } - - get waitlistResourceSignal() { - return waitlistResourceSignal; - } - get waitlistErrorSignal() { - return waitlistErrorSignal; - } - get waitlistFetchSignal() { - return waitlistFetchSignal; - } - get waitlistSignal() { - return waitlistComputedSignal; - } +type ResourceClassWithName = new (...args: any[]) => BaseResource & { + __internal_future: any; + static __internal_resourceName: string; +}; +export class State implements StateInterface { private _waitlistInstance: Waitlist | null = null; __internal_effect = effect; @@ -73,13 +26,45 @@ export class State implements StateInterface { eventBus.on('resource:fetch', this.onResourceFetch); this._waitlistInstance = new Waitlist(null); - this.waitlistResourceSignal({ resource: this._waitlistInstance }); + const waitlistSignalSet = getSignalSetByResourceName('waitlist'); + if (waitlistSignalSet) { + waitlistSignalSet.resourceSignal({ resource: this._waitlistInstance }); + } } get __internal_waitlist() { return this._waitlistInstance; } + getSignalsForResource(resource: BaseResource) { + return getResourceSignalSet(resource); + } + + getSignalsByName(resourceName: string) { + return getSignalSetByResourceName(resourceName); + } + + getSignalsForClass(ResourceClass: T) { + return getSignalSetByResourceName(ResourceClass.__internal_resourceName); + } + + getSignalForResourceName( + resourceName: T, + ): (() => { errors: any; fetchStatus: 'idle' | 'fetching'; [K in T]: any }) | undefined { + const signalSet = getSignalSetByResourceName(resourceName); + return signalSet?.computedSignal; + } + + getSignalForResource(resource: BaseResource) { + const signalSet = getResourceSignalSet(resource); + return signalSet?.computedSignal; + } + + getSignalForClass(ResourceClass: T) { + const signalSet = getSignalSetByResourceName(ResourceClass.__internal_resourceName); + return signalSet?.computedSignal; + } + private onResourceError = (payload: { resource: BaseResource; error: unknown }) => { const signalSet = getResourceSignalSet(payload.resource); if (signalSet) { diff --git a/packages/react/src/hooks/useClerkSignal.ts b/packages/react/src/hooks/useClerkSignal.ts index ba4bbc12de9..e1442f0eaae 100644 --- a/packages/react/src/hooks/useClerkSignal.ts +++ b/packages/react/src/hooks/useClerkSignal.ts @@ -15,12 +15,11 @@ function useClerkSignal(signal: SignalName): SignInSignalValue | SignUpSignalVal const clerk = useIsomorphicClerkContext(); const signalGetter = useMemo(() => { - const map: Record SignInSignalValue | SignUpSignalValue | WaitlistSignalValue> = { - signIn: () => clerk.__internal_state.signInSignal() as SignInSignalValue, - signUp: () => clerk.__internal_state.signUpSignal() as SignUpSignalValue, - waitlist: () => clerk.__internal_state.waitlistSignal() as WaitlistSignalValue, - }; - return map[signal]; + const signalFn = clerk.__internal_state.getSignalForResourceName(signal); + if (!signalFn) { + throw new Error(`Signal not found for resource: ${signal}`); + } + return signalFn as () => SignInSignalValue | SignUpSignalValue | WaitlistSignalValue; }, [clerk.__internal_state, signal]); const subscribe = useCallback(