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
5 changes: 2 additions & 3 deletions packages/core/src/query/queryStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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, {
Expand Down
47 changes: 13 additions & 34 deletions packages/core/src/releases/getPerspectiveState.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -20,6 +20,7 @@ vi.mock('../client/clientStore', () => ({
}))

describe('getPerspectiveState', () => {
const source = sourceFor({projectId: 'test', dataset: 'test'})
let instance: SanityInstance
let mockReleasesQuerySubject: Subject<ReleaseDocument[]>

Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down
20 changes: 10 additions & 10 deletions packages/core/src/releases/getPerspectiveState.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<string, Map<string, PerspectiveHandle>>()

const selectInstancePerspective = (context: SelectorContext<ReleasesStoreState>) =>
context.instance.config.perspective
const selectActiveReleases = (context: SelectorContext<ReleasesStoreState>) =>
context.state.activeReleases
const selectOptions = (
_context: SelectorContext<ReleasesStoreState>,
options: PerspectiveHandle & {projectId?: string; dataset?: string},
options: {perspective: ClientPerspective | ReleasePerspective; source: DocumentSource},
) => options

const memoizedOptionsSelector = createSelector(
Expand Down Expand Up @@ -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

Expand Down
39 changes: 18 additions & 21 deletions packages/react/src/hooks/releases/usePerspective.ts
Original file line number Diff line number Diff line change
@@ -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[]
}

/**
Expand All @@ -30,21 +25,23 @@ 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<Movie[]>('*[_type == "movie"]', {
* perspective: perspective,
* })
* ```
*
* @returns The perspective for the given perspective handle.
*/
export const usePerspective: UsePerspective = createStateSourceHook({
getState: getPerspectiveState as (
instance: SanityInstance,
perspectiveHandle?: PerspectiveHandle,
) => StateSource<string | string[]>,
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)
}
Loading