Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/resources/Waitlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
146 changes: 105 additions & 41 deletions packages/clerk-js/src/core/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,115 @@ 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';

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 };
});
import { SignIn } from './resources/SignIn';
import { SignUp } from './resources/SignUp';
import { Waitlist } from './resources/Waitlist';
import type { BaseResource } from './resources/Base';

interface ResourceSignalSet<TResource extends { __internal_future: any }, TComputedSignal> {
resourceSignal: ReturnType<typeof signal<{ resource: TResource | null }>>;
errorSignal: ReturnType<typeof signal<{ error: unknown }>>;
fetchSignal: ReturnType<typeof signal<{ status: 'idle' | 'fetching' }>>;
computedSignal: TComputedSignal;
}

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' });
type ResourceClass<T extends BaseResource = BaseResource> = new (...args: any[]) => T & {
__internal_future: any;
static __internal_resourceName: string;
};

type ResourceName<T extends ResourceClass> = T['__internal_resourceName'];

function createResourceSignalSet<
TResource extends { __internal_future: any },
TSignalName extends string,
TComputedSignal extends () => { errors: Errors; fetchStatus: 'idle' | 'fetching'; [K in TSignalName]: any },
>(
resourceName: TSignalName,
): ResourceSignalSet<TResource, TComputedSignal> {
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<TComputedSignal>;
}) as TComputedSignal;

return {
resourceSignal,
errorSignal,
fetchSignal,
computedSignal,
};
}

export const waitlistComputedSignal: WaitlistSignal = computed(() => {
const waitlist = waitlistResourceSignal().resource;
const error = waitlistErrorSignal().error;
const fetchStatus = waitlistFetchSignal().status;
const resourceSignalRegistry = new Map<
ResourceClass,
ResourceSignalSet<any, any>
>();

const resourceNameToSignalSet = new Map<string, ResourceSignalSet<any, any>>();

function registerResourceSignals<
TResourceClass extends ResourceClass,
TResource extends InstanceType<TResourceClass>,
TSignalName extends ResourceName<TResourceClass>,
>(
ResourceClass: TResourceClass & { __internal_resourceName: TSignalName },
signalType: () => { errors: Errors; fetchStatus: 'idle' | 'fetching'; [K in TSignalName]: any },
): ResourceSignalSet<TResource, ReturnType<typeof signalType>> {
const resourceName = ResourceClass.__internal_resourceName;
const signalSet = createResourceSignalSet<TResource, TSignalName, ReturnType<typeof signalType>>(
resourceName,
);
resourceSignalRegistry.set(ResourceClass as ResourceClass, signalSet);
resourceNameToSignalSet.set(resourceName, signalSet);
return signalSet;
}

const errors = errorsToParsedErrors(error);
export function getSignalSetByResourceName(
resourceName: string,
): ResourceSignalSet<any, any> | undefined {
return resourceNameToSignalSet.get(resourceName);
}

return { errors, fetchStatus, waitlist: waitlist ? waitlist.__internal_future : null };
});
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 = 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 = 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<any, any> | 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
Expand Down
107 changes: 48 additions & 59 deletions packages/clerk-js/src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +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 {
signInComputedSignal,
signInErrorSignal,
signInFetchSignal,
signInResourceSignal,
signUpComputedSignal,
signUpErrorSignal,
signUpFetchSignal,
signUpResourceSignal,
waitlistComputedSignal,
waitlistErrorSignal,
waitlistFetchSignal,
waitlistResourceSignal,
getResourceSignalSet,
getSignalSetByResourceName,
} from './signals';

export class State implements StateInterface {
signInResourceSignal = signInResourceSignal;
signInErrorSignal = signInErrorSignal;
signInFetchSignal = signInFetchSignal;
signInSignal = signInComputedSignal;

signUpResourceSignal = signUpResourceSignal;
signUpErrorSignal = signUpErrorSignal;
signUpFetchSignal = signUpFetchSignal;
signUpSignal = signUpComputedSignal;

waitlistResourceSignal = waitlistResourceSignal;
waitlistErrorSignal = waitlistErrorSignal;
waitlistFetchSignal = waitlistFetchSignal;
waitlistSignal = 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;
Expand All @@ -48,52 +26,63 @@ 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;
}

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ error: payload.error });
}
getSignalsForResource(resource: BaseResource) {
return getResourceSignalSet(resource);
}

if (payload.resource instanceof SignUp) {
this.signUpErrorSignal({ error: payload.error });
}
getSignalsByName(resourceName: string) {
return getSignalSetByResourceName(resourceName);
}

if (payload.resource instanceof Waitlist) {
this.waitlistErrorSignal({ error: payload.error });
}
};
getSignalsForClass<T extends ResourceClassWithName>(ResourceClass: T) {
return getSignalSetByResourceName(ResourceClass.__internal_resourceName);
}

private onResourceUpdated = (payload: { resource: BaseResource }) => {
if (payload.resource instanceof SignIn) {
this.signInResourceSignal({ resource: payload.resource });
}
getSignalForResourceName<T extends string>(
resourceName: T,
): (() => { errors: any; fetchStatus: 'idle' | 'fetching'; [K in T]: any }) | undefined {
const signalSet = getSignalSetByResourceName(resourceName);
return signalSet?.computedSignal;
}

if (payload.resource instanceof SignUp) {
this.signUpResourceSignal({ resource: payload.resource });
}
getSignalForResource(resource: BaseResource) {
const signalSet = getResourceSignalSet(resource);
return signalSet?.computedSignal;
}

if (payload.resource instanceof Waitlist) {
this.waitlistResourceSignal({ resource: payload.resource });
}
};
getSignalForClass<T extends ResourceClassWithName>(ResourceClass: T) {
const signalSet = getSignalSetByResourceName(ResourceClass.__internal_resourceName);
return signalSet?.computedSignal;
}

private onResourceFetch = (payload: { resource: BaseResource; status: 'idle' | 'fetching' }) => {
if (payload.resource instanceof SignIn) {
this.signInFetchSignal({ status: payload.status });
private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
const signalSet = getResourceSignalSet(payload.resource);
if (signalSet) {
signalSet.errorSignal({ error: payload.error });
}
};

if (payload.resource instanceof SignUp) {
this.signUpFetchSignal({ status: payload.status });
private onResourceUpdated = (payload: { resource: BaseResource }) => {
const signalSet = getResourceSignalSet(payload.resource);
if (signalSet) {
signalSet.resourceSignal({ resource: payload.resource });
}
};

if (payload.resource instanceof Waitlist) {
this.waitlistFetchSignal({ status: payload.status });
private onResourceFetch = (payload: { resource: BaseResource; status: 'idle' | 'fetching' }) => {
const signalSet = getResourceSignalSet(payload.resource);
if (signalSet) {
signalSet.fetchSignal({ status: payload.status });
}
};
}
44 changes: 17 additions & 27 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,44 @@
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 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(
(callback: () => void) => {
if (!clerk.loaded) {
return () => {};
}

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);

Expand Down
Loading