diff --git a/packages/core/src/query/queryStore.ts b/packages/core/src/query/queryStore.ts index 9d62f8687..1bd4c42fd 100644 --- a/packages/core/src/query/queryStore.ts +++ b/packages/core/src/query/queryStore.ts @@ -23,7 +23,7 @@ import { } from 'rxjs' import {getClientState} from '../client/clientStore' -import {type DocumentSource, type ReleasePerspective} from '../config/sanityConfig' +import {type DocumentSource, type ReleasePerspective, sourceFor} from '../config/sanityConfig' import {getPerspectiveState} from '../releases/getPerspectiveState' import {bindActionByDataset, type BoundDatasetKey} from '../store/createActionBinder' import {type SanityInstance} from '../store/createSanityInstance' @@ -137,8 +137,7 @@ const listenForNewSubscribersAndFetch = ({ const perspective$ = getPerspectiveState(instance, { perspective: perspectiveFromOptions, - projectId, - dataset, + source: sourceFor({projectId, dataset}), }).observable.pipe(filter(Boolean)) const client$ = getClientState(instance, { diff --git a/packages/core/src/releases/getPerspectiveState.test.ts b/packages/core/src/releases/getPerspectiveState.test.ts index 2f8f27739..70a8c9ffb 100644 --- a/packages/core/src/releases/getPerspectiveState.test.ts +++ b/packages/core/src/releases/getPerspectiveState.test.ts @@ -1,7 +1,7 @@ import {filter, firstValueFrom, of, Subject, take} from 'rxjs' import {describe, expect, it, vi} from 'vitest' -import {type PerspectiveHandle, type ReleasePerspective} from '../config/sanityConfig' +import {type ReleasePerspective, sourceFor} from '../config/sanityConfig' import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance' import {listenQuery as mockListenQuery} from '../utils/listenQuery' import {getPerspectiveState} from './getPerspectiveState' @@ -20,6 +20,7 @@ vi.mock('../client/clientStore', () => ({ })) describe('getPerspectiveState', () => { + const source = sourceFor({projectId: 'test', dataset: 'test'}) let instance: SanityInstance let mockReleasesQuerySubject: Subject @@ -53,40 +54,22 @@ describe('getPerspectiveState', () => { vi.clearAllMocks() }) - it('should return default perspective if no options or instance perspective is provided', async () => { - const state = getPerspectiveState(instance, {}) - mockReleasesQuerySubject.next([]) - const perspective = await firstValueFrom(state.observable) - expect(perspective).toBe('drafts') - }) - - it('should return instance perspective if provided and no options perspective', async () => { - instance.config.perspective = 'published' - const state = getPerspectiveState(instance, {}) - mockReleasesQuerySubject.next([]) - const perspective = await firstValueFrom(state.observable) - expect(perspective).toBe('published') - }) - it('should return options perspective if provided', async () => { - const options: PerspectiveHandle = {perspective: 'raw'} - const state = getPerspectiveState(instance, options) + const state = getPerspectiveState(instance, {perspective: 'raw', source}) mockReleasesQuerySubject.next([]) const perspective = await firstValueFrom(state.observable) expect(perspective).toBe('raw') }) it('should return undefined if release perspective is requested but no active releases', async () => { - const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}} - const state = getPerspectiveState(instance, options) + const state = getPerspectiveState(instance, {perspective: {releaseName: 'release1'}, source}) mockReleasesQuerySubject.next([]) const perspective = await firstValueFrom(state.observable) expect(perspective).toBeUndefined() }) it('should calculate perspective based on active releases and releaseName', async () => { - const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}} - const state = getPerspectiveState(instance, options) + const state = getPerspectiveState(instance, {perspective: {releaseName: 'release1'}, source}) mockReleasesQuerySubject.next(activeReleases) const perspective = await firstValueFrom( @@ -99,8 +82,7 @@ describe('getPerspectiveState', () => { }) it('should calculate perspective including multiple releases up to the specified releaseName', async () => { - const options: PerspectiveHandle = {perspective: {releaseName: 'release2'}} - const state = getPerspectiveState(instance, options) + const state = getPerspectiveState(instance, {perspective: {releaseName: 'release2'}, source}) mockReleasesQuerySubject.next(activeReleases) const perspective = await firstValueFrom( state.observable.pipe( @@ -116,8 +98,7 @@ describe('getPerspectiveState', () => { releaseName: 'release2', excludedPerspectives: ['drafts', 'release1'], } - const options: PerspectiveHandle = {perspective: perspectiveConfig} - const state = getPerspectiveState(instance, options) + const state = getPerspectiveState(instance, {perspective: perspectiveConfig, source}) mockReleasesQuerySubject.next(activeReleases) const perspective = await firstValueFrom( state.observable.pipe( @@ -129,8 +110,7 @@ describe('getPerspectiveState', () => { }) it('should throw if the specified releaseName is not found in active releases', async () => { - const options: PerspectiveHandle = {perspective: {releaseName: 'nonexistent'}} - const state = getPerspectiveState(instance, options) + const state = getPerspectiveState(instance, {perspective: {releaseName: 'nonexistent'}, source}) mockReleasesQuerySubject.next(activeReleases) await expect( @@ -144,10 +124,10 @@ describe('getPerspectiveState', () => { }) it('should reuse the same options object for identical inputs (cache test)', async () => { - const options1: PerspectiveHandle = {perspective: {releaseName: 'release1'}} - const options2: PerspectiveHandle = {perspective: {releaseName: 'release1'}} + const options1 = {perspective: {releaseName: 'release1'}} + const options2 = {perspective: {releaseName: 'release1'}} - const state1 = getPerspectiveState(instance, options1) + const state1 = getPerspectiveState(instance, {...options1, source}) mockReleasesQuerySubject.next(activeReleases) await firstValueFrom( state1.observable.pipe( @@ -156,15 +136,14 @@ describe('getPerspectiveState', () => { ), ) - const state2 = getPerspectiveState(instance, options2) + const state2 = getPerspectiveState(instance, {...options2, source}) const perspective2 = state2.getCurrent() expect(perspective2).toEqual(['drafts', 'release1']) }) it('should handle changes in activeReleases (cache test)', async () => { - const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}} - + const options = {perspective: {releaseName: 'release1'}, source} const state1 = getPerspectiveState(instance, options) mockReleasesQuerySubject.next(activeReleases) const perspective1 = await firstValueFrom( diff --git a/packages/core/src/releases/getPerspectiveState.ts b/packages/core/src/releases/getPerspectiveState.ts index ccaa46d04..a8cc2cb6b 100644 --- a/packages/core/src/releases/getPerspectiveState.ts +++ b/packages/core/src/releases/getPerspectiveState.ts @@ -1,6 +1,11 @@ +import {type ClientPerspective} from '@sanity/client' import {createSelector} from 'reselect' -import {type PerspectiveHandle, type ReleasePerspective} from '../config/sanityConfig' +import { + type DocumentSource, + type PerspectiveHandle, + type ReleasePerspective, +} from '../config/sanityConfig' import {bindActionByDataset} from '../store/createActionBinder' import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction' import {releasesStore, type ReleasesStoreState} from './releasesStore' @@ -12,18 +17,14 @@ function isReleasePerspective( return typeof perspective === 'object' && perspective !== null && 'releaseName' in perspective } -const DEFAULT_PERSPECTIVE = 'drafts' - // Cache for options const optionsCache = new Map>() -const selectInstancePerspective = (context: SelectorContext) => - context.instance.config.perspective const selectActiveReleases = (context: SelectorContext) => context.state.activeReleases const selectOptions = ( _context: SelectorContext, - options: PerspectiveHandle & {projectId?: string; dataset?: string}, + options: {perspective: ClientPerspective | ReleasePerspective; source: DocumentSource}, ) => options const memoizedOptionsSelector = createSelector( @@ -66,10 +67,9 @@ export const getPerspectiveState = bindActionByDataset( releasesStore, createStateSourceAction({ selector: createSelector( - [selectInstancePerspective, selectActiveReleases, memoizedOptionsSelector], - (instancePerspective, activeReleases, memoizedOptions) => { - const perspective = - memoizedOptions?.perspective ?? instancePerspective ?? DEFAULT_PERSPECTIVE + [selectActiveReleases, memoizedOptionsSelector], + (activeReleases, memoizedOptions) => { + const perspective = memoizedOptions.perspective if (!isReleasePerspective(perspective)) return perspective diff --git a/packages/react/src/hooks/releases/usePerspective.ts b/packages/react/src/hooks/releases/usePerspective.ts index 568ff0d4e..32d1875be 100644 --- a/packages/react/src/hooks/releases/usePerspective.ts +++ b/packages/react/src/hooks/releases/usePerspective.ts @@ -1,19 +1,14 @@ -import { - getActiveReleasesState, - getPerspectiveState, - type PerspectiveHandle, - type SanityInstance, - type StateSource, -} from '@sanity/sdk' -import {filter, firstValueFrom} from 'rxjs' +import {type DocumentSource, getPerspectiveState, type PerspectiveHandle} from '@sanity/sdk' +import {useMemo} from 'react' -import {createStateSourceHook} from '../helpers/createStateSourceHook' +import {useSanityInstanceAndSource} from '../context/useSanityInstance' +import {useStoreState} from '../helpers/useStoreState' /** * @public */ type UsePerspective = { - (perspectiveHandle: PerspectiveHandle): string | string[] + (perspectiveHandle: PerspectiveHandle & {source?: DocumentSource}): string | string[] } /** @@ -30,7 +25,7 @@ type UsePerspective = { * ```tsx * import {usePerspective, useQuery} from '@sanity/sdk-react' - * const perspective = usePerspective({perspective: 'rxg1346', projectId: 'abc123', dataset: 'production'}) + * const perspective = usePerspective({perspective: 'rxg1346'}) * const {data} = useQuery('*[_type == "movie"]', { * perspective: perspective, * }) @@ -38,13 +33,15 @@ type UsePerspective = { * * @returns The perspective for the given perspective handle. */ -export const usePerspective: UsePerspective = createStateSourceHook({ - getState: getPerspectiveState as ( - instance: SanityInstance, - perspectiveHandle?: PerspectiveHandle, - ) => StateSource, - shouldSuspend: (instance: SanityInstance, options: PerspectiveHandle): boolean => - getPerspectiveState(instance, options).getCurrent() === undefined, - suspender: (instance: SanityInstance, _options?: PerspectiveHandle) => - firstValueFrom(getActiveReleasesState(instance, {}).observable.pipe(filter(Boolean))), -}) +export const usePerspective: UsePerspective = ({perspective, source}) => { + const [instance, actualSource] = useSanityInstanceAndSource({source}) + + const actualPerspective = perspective ?? instance.config.perspective ?? 'drafts' + + const state = useMemo( + () => getPerspectiveState(instance, {perspective: actualPerspective, source: actualSource}), + [instance, actualPerspective, actualSource], + ) + + return useStoreState(state) +}