@sanity/sdk 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/index.d.ts +124 -13
  2. package/dist/index.js +468 -243
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -4
  5. package/src/_exports/index.ts +3 -0
  6. package/src/auth/authMode.test.ts +56 -0
  7. package/src/auth/authMode.ts +71 -0
  8. package/src/auth/authStore.test.ts +85 -4
  9. package/src/auth/authStore.ts +63 -125
  10. package/src/auth/authStrategy.ts +39 -0
  11. package/src/auth/dashboardAuth.ts +132 -0
  12. package/src/auth/standaloneAuth.ts +109 -0
  13. package/src/auth/studioAuth.ts +217 -0
  14. package/src/auth/studioModeAuth.test.ts +43 -1
  15. package/src/auth/studioModeAuth.ts +10 -1
  16. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
  17. package/src/config/sanityConfig.ts +48 -7
  18. package/src/projection/getProjectionState.ts +6 -5
  19. package/src/projection/projectionQuery.test.ts +38 -55
  20. package/src/projection/projectionQuery.ts +27 -31
  21. package/src/projection/projectionStore.test.ts +4 -4
  22. package/src/projection/projectionStore.ts +3 -2
  23. package/src/projection/resolveProjection.ts +2 -2
  24. package/src/projection/statusQuery.test.ts +35 -0
  25. package/src/projection/statusQuery.ts +71 -0
  26. package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
  27. package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
  28. package/src/projection/types.ts +12 -0
  29. package/src/projection/util.ts +0 -1
  30. package/src/query/queryStore.test.ts +64 -0
  31. package/src/query/queryStore.ts +30 -10
  32. package/src/releases/getPerspectiveState.test.ts +17 -14
  33. package/src/releases/getPerspectiveState.ts +58 -38
  34. package/src/releases/releasesStore.test.ts +59 -61
  35. package/src/releases/releasesStore.ts +21 -35
  36. package/src/releases/utils/isReleasePerspective.ts +7 -0
  37. package/src/store/createActionBinder.test.ts +211 -1
  38. package/src/store/createActionBinder.ts +95 -17
  39. package/src/store/createSanityInstance.ts +3 -1
@@ -2,14 +2,13 @@ import {filter, firstValueFrom, of, Subject, take} from 'rxjs'
2
2
  import {describe, expect, it, vi} from 'vitest'
3
3
 
4
4
  import {type PerspectiveHandle, type ReleasePerspective} from '../config/sanityConfig'
5
+ import {getQueryState} from '../query/queryStore'
5
6
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
- import {listenQuery as mockListenQuery} from '../utils/listenQuery'
7
+ import {type StateSource} from '../store/createStateSourceAction'
7
8
  import {getPerspectiveState} from './getPerspectiveState'
8
9
  import {type ReleaseDocument} from './releasesStore'
9
10
 
10
- vi.mock('../utils/listenQuery', () => ({
11
- listenQuery: vi.fn(),
12
- }))
11
+ vi.mock('../query/queryStore')
13
12
 
14
13
  vi.mock('../client/clientStore', () => ({
15
14
  getClientState: vi.fn(() => ({
@@ -21,7 +20,7 @@ vi.mock('../client/clientStore', () => ({
21
20
 
22
21
  describe('getPerspectiveState', () => {
23
22
  let instance: SanityInstance
24
- let mockReleasesQuerySubject: Subject<ReleaseDocument[]>
23
+ let mockReleasesQuerySubject: Subject<ReleaseDocument[] | undefined>
25
24
 
26
25
  const release1 = {
27
26
  _id: 'release-1',
@@ -44,8 +43,12 @@ describe('getPerspectiveState', () => {
44
43
  beforeEach(() => {
45
44
  instance = createSanityInstance({projectId: 'test', dataset: 'test'})
46
45
 
47
- mockReleasesQuerySubject = new Subject<ReleaseDocument[]>()
48
- vi.mocked(mockListenQuery).mockReturnValue(mockReleasesQuerySubject.asObservable())
46
+ mockReleasesQuerySubject = new Subject<ReleaseDocument[] | undefined>()
47
+ vi.mocked(getQueryState).mockReturnValue({
48
+ subscribe: () => () => {},
49
+ getCurrent: () => undefined,
50
+ observable: mockReleasesQuerySubject.asObservable(),
51
+ } as StateSource<ReleaseDocument[] | undefined>)
49
52
  })
50
53
 
51
54
  afterEach(() => {
@@ -95,7 +98,7 @@ describe('getPerspectiveState', () => {
95
98
  take(1),
96
99
  ),
97
100
  )
98
- expect(perspective).toEqual(['drafts', 'release1'])
101
+ expect(perspective).toEqual(['release1', 'drafts'])
99
102
  })
100
103
 
101
104
  it('should calculate perspective including multiple releases up to the specified releaseName', async () => {
@@ -108,13 +111,13 @@ describe('getPerspectiveState', () => {
108
111
  take(1),
109
112
  ),
110
113
  )
111
- expect(perspective).toEqual(['drafts', 'release1', 'release2'])
114
+ expect(perspective).toEqual(['release2', 'release1', 'drafts'])
112
115
  })
113
116
 
114
117
  it('should filter excluded perspectives', async () => {
115
118
  const perspectiveConfig: ReleasePerspective = {
116
119
  releaseName: 'release2',
117
- excludedPerspectives: ['drafts', 'release1'],
120
+ excludedPerspectives: ['release1', 'drafts'],
118
121
  }
119
122
  const options: PerspectiveHandle = {perspective: perspectiveConfig}
120
123
  const state = getPerspectiveState(instance, options)
@@ -159,7 +162,7 @@ describe('getPerspectiveState', () => {
159
162
  const state2 = getPerspectiveState(instance, options2)
160
163
  const perspective2 = state2.getCurrent()
161
164
 
162
- expect(perspective2).toEqual(['drafts', 'release1'])
165
+ expect(perspective2).toEqual(['release1', 'drafts'])
163
166
  })
164
167
 
165
168
  it('should handle changes in activeReleases (cache test)', async () => {
@@ -173,7 +176,7 @@ describe('getPerspectiveState', () => {
173
176
  take(1),
174
177
  ),
175
178
  )
176
- expect(perspective1).toEqual(['drafts', 'release1'])
179
+ expect(perspective1).toEqual(['release1', 'drafts'])
177
180
 
178
181
  const updatedActiveReleases = [release1]
179
182
  mockReleasesQuerySubject.next(updatedActiveReleases)
@@ -184,10 +187,10 @@ describe('getPerspectiveState', () => {
184
187
  take(1),
185
188
  ),
186
189
  )
187
- expect(perspectiveAfterUpdate).toEqual(['drafts', 'release1'])
190
+ expect(perspectiveAfterUpdate).toEqual(['release1', 'drafts'])
188
191
 
189
192
  const state2 = getPerspectiveState(instance, options)
190
193
  const perspectiveNewCall = state2.getCurrent()
191
- expect(perspectiveNewCall).toEqual(['drafts', 'release1'])
194
+ expect(perspectiveNewCall).toEqual(['release1', 'drafts'])
192
195
  })
193
196
  })
@@ -1,17 +1,20 @@
1
1
  import {createSelector} from 'reselect'
2
2
 
3
- import {type PerspectiveHandle, type ReleasePerspective} from '../config/sanityConfig'
4
- import {bindActionByDataset} from '../store/createActionBinder'
3
+ import {type PerspectiveHandle} from '../config/sanityConfig'
4
+ import {bindActionByDataset, type BoundStoreAction} from '../store/createActionBinder'
5
5
  import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
6
+ /*
7
+ * Although this is an import dependency cycle, it is not a logical cycle:
8
+ * 1. getPerspectiveState uses releasesStore as a data source
9
+ * 2. releasesStore uses queryStore as a data source
10
+ * 3. queryStore calls getPerspectiveState for computing release perspectives
11
+ * 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
12
+ */
13
+ // eslint-disable-next-line import/no-cycle
6
14
  import {releasesStore, type ReleasesStoreState} from './releasesStore'
15
+ import {isReleasePerspective} from './utils/isReleasePerspective'
7
16
  import {sortReleases} from './utils/sortReleases'
8
17
 
9
- function isReleasePerspective(
10
- perspective: PerspectiveHandle['perspective'],
11
- ): perspective is ReleasePerspective {
12
- return typeof perspective === 'object' && perspective !== null && 'releaseName' in perspective
13
- }
14
-
15
18
  const DEFAULT_PERSPECTIVE = 'drafts'
16
19
 
17
20
  // Cache for options
@@ -50,6 +53,44 @@ const memoizedOptionsSelector = createSelector(
50
53
  },
51
54
  )
52
55
 
56
+ // Lazily bind the action itself to avoid circular import initialization issues with `releasesStore`
57
+ const _getPerspectiveStateSelector = createStateSourceAction({
58
+ selector: createSelector(
59
+ [selectInstancePerspective, selectActiveReleases, memoizedOptionsSelector],
60
+ (instancePerspective, activeReleases, memoizedOptions) => {
61
+ const perspective = memoizedOptions?.perspective ?? instancePerspective ?? DEFAULT_PERSPECTIVE
62
+
63
+ if (!isReleasePerspective(perspective)) return perspective
64
+
65
+ // if there are no active releases we can't compute the release perspective
66
+ if (!activeReleases || activeReleases.length === 0) return undefined
67
+
68
+ const releaseNames = sortReleases(activeReleases).map((release) => release.name)
69
+ const index = releaseNames.findIndex((name) => name === perspective.releaseName)
70
+
71
+ if (index < 0) {
72
+ throw new Error(`Release "${perspective.releaseName}" not found in active releases`)
73
+ }
74
+
75
+ const filteredReleases = releaseNames.slice(0, index + 1) // Include the release itself
76
+
77
+ return ['drafts', ...filteredReleases]
78
+ .filter((name) => !perspective.excludedPerspectives?.includes(name))
79
+ .reverse()
80
+ },
81
+ ),
82
+ })
83
+
84
+ type OmitFirst<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never
85
+ type SelectorParams = OmitFirst<Parameters<typeof _getPerspectiveStateSelector>>
86
+ type BoundGetPerspectiveState = BoundStoreAction<
87
+ ReleasesStoreState,
88
+ SelectorParams,
89
+ ReturnType<typeof _getPerspectiveStateSelector>
90
+ >
91
+
92
+ let _boundGetPerspectiveState: BoundGetPerspectiveState | undefined
93
+
53
94
  /**
54
95
  * Provides a subscribable state source for a "perspective" for the Sanity client,
55
96
  * which is used to fetch documents as though certain Content Releases are active.
@@ -62,33 +103,12 @@ const memoizedOptionsSelector = createSelector(
62
103
  *
63
104
  * @public
64
105
  */
65
- export const getPerspectiveState = bindActionByDataset(
66
- releasesStore,
67
- createStateSourceAction({
68
- selector: createSelector(
69
- [selectInstancePerspective, selectActiveReleases, memoizedOptionsSelector],
70
- (instancePerspective, activeReleases, memoizedOptions) => {
71
- const perspective =
72
- memoizedOptions?.perspective ?? instancePerspective ?? DEFAULT_PERSPECTIVE
73
-
74
- if (!isReleasePerspective(perspective)) return perspective
75
-
76
- // if there are no active releases we can't compute the release perspective
77
- if (!activeReleases || activeReleases.length === 0) return undefined
78
-
79
- const releaseNames = sortReleases(activeReleases).map((release) => release.name)
80
- const index = releaseNames.findIndex((name) => name === perspective.releaseName)
81
-
82
- if (index < 0) {
83
- throw new Error(`Release "${perspective.releaseName}" not found in active releases`)
84
- }
85
-
86
- const filteredReleases = releaseNames.slice(0, index + 1) // Include the release itself
87
-
88
- return ['drafts', ...filteredReleases].filter(
89
- (name) => !perspective.excludedPerspectives?.includes(name),
90
- )
91
- },
92
- ),
93
- }),
94
- )
106
+ export const getPerspectiveState: BoundGetPerspectiveState = (...args) => {
107
+ if (!_boundGetPerspectiveState) {
108
+ _boundGetPerspectiveState = bindActionByDataset(
109
+ releasesStore,
110
+ _getPerspectiveStateSelector,
111
+ ) as BoundGetPerspectiveState
112
+ }
113
+ return _boundGetPerspectiveState(...args)
114
+ }
@@ -1,45 +1,47 @@
1
- import {type SanityClient} from '@sanity/client'
2
- import {of, Subject} from 'rxjs'
1
+ import {NEVER, Observable, type Observer, of, Subject} from 'rxjs'
3
2
  import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
4
3
 
5
- import {getClientState} from '../client/clientStore'
4
+ import {getQueryState, resolveQuery} from '../query/queryStore'
6
5
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
6
  import {type StateSource} from '../store/createStateSourceAction'
8
- import {listenQuery} from '../utils/listenQuery'
9
7
  import {getActiveReleasesState, type ReleaseDocument} from './releasesStore'
10
8
 
11
9
  // Mock dependencies
12
- vi.mock('../client/clientStore', () => ({
13
- getClientState: vi.fn(),
14
- }))
15
- vi.mock('../utils/listenQuery', () => ({
16
- listenQuery: vi.fn(),
17
- }))
18
-
19
- // Mock console.error to prevent test runner noise and allow verification
20
- let consoleErrorSpy: ReturnType<typeof vi.spyOn>
10
+ vi.mock('../query/queryStore')
21
11
 
22
12
  describe('releasesStore', () => {
23
13
  let instance: SanityInstance
24
- const mockClient = {} as SanityClient
25
14
 
26
15
  beforeEach(() => {
27
- vi.resetAllMocks()
28
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
16
+ vi.clearAllMocks()
29
17
 
30
18
  instance = createSanityInstance({projectId: 'test', dataset: 'test'})
31
19
 
32
- vi.mocked(getClientState).mockReturnValue({
33
- observable: of(mockClient),
34
- } as StateSource<SanityClient>)
20
+ vi.mocked(getQueryState).mockReturnValue({
21
+ subscribe: () => () => {},
22
+ getCurrent: () => undefined,
23
+ observable: NEVER as Observable<ReleaseDocument[] | undefined>,
24
+ } as StateSource<ReleaseDocument[] | undefined>)
25
+
26
+ vi.mocked(resolveQuery).mockResolvedValue(undefined)
35
27
  })
36
28
 
37
29
  afterEach(() => {
38
30
  instance.dispose()
39
- consoleErrorSpy.mockRestore()
40
31
  })
41
32
 
42
- it('should set active releases state when listenQuery succeeds', async () => {
33
+ it('should set active releases state when the releases query emits', async () => {
34
+ const teardown = vi.fn()
35
+ const subscriber = vi
36
+ .fn<(observer: Observer<ReleaseDocument[] | undefined>) => () => void>()
37
+ .mockReturnValue(teardown)
38
+
39
+ vi.mocked(getQueryState).mockReturnValue({
40
+ subscribe: () => () => {},
41
+ getCurrent: () => undefined,
42
+ observable: new Observable(subscriber),
43
+ } as StateSource<ReleaseDocument[] | undefined>)
44
+
43
45
  // note that the order of the releases is important here -- they get sorted
44
46
  const mockReleases: ReleaseDocument[] = [
45
47
  {
@@ -56,21 +58,24 @@ describe('releasesStore', () => {
56
58
  } as ReleaseDocument,
57
59
  ]
58
60
 
59
- vi.mocked(listenQuery).mockReturnValue(of(mockReleases))
60
-
61
61
  const state = getActiveReleasesState(instance)
62
62
 
63
+ const [observer] = subscriber.mock.lastCall!
64
+
65
+ observer.next(mockReleases)
66
+
63
67
  await new Promise((resolve) => setTimeout(resolve, 0))
64
68
 
65
69
  expect(state.getCurrent()).toEqual(mockReleases.reverse())
66
- expect(consoleErrorSpy).not.toHaveBeenCalled()
67
-
68
- vi.useRealTimers()
69
70
  })
70
71
 
71
- it('should update active releases state when listenQuery emits new data', async () => {
72
+ it('should update active releases state when the query emits new data', async () => {
72
73
  const releasesSubject = new Subject<ReleaseDocument[]>()
73
- vi.mocked(listenQuery).mockReturnValue(releasesSubject.asObservable())
74
+ vi.mocked(getQueryState).mockReturnValue({
75
+ subscribe: () => () => {},
76
+ getCurrent: () => undefined,
77
+ observable: releasesSubject.asObservable(),
78
+ } as StateSource<ReleaseDocument[] | undefined>)
74
79
 
75
80
  const state = getActiveReleasesState(instance)
76
81
 
@@ -109,65 +114,58 @@ describe('releasesStore', () => {
109
114
  await new Promise((resolve) => setTimeout(resolve, 0))
110
115
 
111
116
  expect(state.getCurrent()).toEqual(updatedReleases.reverse())
112
- expect(consoleErrorSpy).not.toHaveBeenCalled()
113
117
  })
114
118
 
115
- it('should handle empty array from listenQuery', async () => {
116
- // Configure listenQuery to return an empty array
117
- vi.mocked(listenQuery).mockReturnValue(of([]))
119
+ it('should handle empty array from the query', async () => {
120
+ // Configure query to return an empty array
121
+ vi.mocked(getQueryState).mockReturnValue({
122
+ subscribe: () => () => {},
123
+ getCurrent: () => [],
124
+ observable: of([]),
125
+ } as StateSource<ReleaseDocument[] | undefined>)
118
126
 
119
127
  const state = getActiveReleasesState(instance)
120
128
 
121
129
  await new Promise((resolve) => setTimeout(resolve, 0))
122
130
 
123
131
  expect(state.getCurrent()).toEqual([]) // Should be set to empty array
124
- expect(consoleErrorSpy).not.toHaveBeenCalled()
125
132
  })
126
133
 
127
- it('should handle null/undefined from listenQuery by defaulting to empty array', async () => {
134
+ it('should handle null/undefined from the query by defaulting to empty array', async () => {
128
135
  // Test null case
129
- vi.mocked(listenQuery).mockReturnValue(of(null))
136
+ vi.mocked(getQueryState).mockReturnValue({
137
+ subscribe: () => () => {},
138
+ getCurrent: () => null as unknown as ReleaseDocument[] | undefined,
139
+ observable: of(null as unknown as ReleaseDocument[] | undefined),
140
+ } as StateSource<ReleaseDocument[] | undefined>)
130
141
  const state = getActiveReleasesState(instance)
131
142
  await new Promise((resolve) => setTimeout(resolve, 0))
132
143
  expect(state.getCurrent()).toEqual([])
133
- expect(consoleErrorSpy).not.toHaveBeenCalled()
134
144
 
135
145
  // Test undefined case
136
- vi.mocked(listenQuery).mockReturnValue(of(undefined))
146
+ vi.mocked(getQueryState).mockReturnValue({
147
+ subscribe: () => () => {},
148
+ getCurrent: () => undefined,
149
+ observable: of(undefined),
150
+ } as StateSource<ReleaseDocument[] | undefined>)
137
151
  await new Promise((resolve) => setTimeout(resolve, 0))
138
152
  expect(state.getCurrent()).toEqual([])
139
- expect(consoleErrorSpy).not.toHaveBeenCalled()
140
153
  })
141
154
 
142
- it('should handle errors from listenQuery by retrying and eventually setting error state', async () => {
143
- vi.useFakeTimers()
144
- const error = new Error('Query failed')
155
+ it('should not crash when the releases query errors', async () => {
145
156
  const subject = new Subject<ReleaseDocument[]>()
146
- vi.mocked(listenQuery).mockReturnValue(subject.asObservable())
157
+ vi.mocked(getQueryState).mockReturnValue({
158
+ subscribe: () => () => {},
159
+ getCurrent: () => undefined,
160
+ observable: subject.asObservable(),
161
+ } as StateSource<ReleaseDocument[] | undefined>)
147
162
 
148
- // initialize the store
149
163
  const state = getActiveReleasesState(instance)
150
164
 
151
- // Error the subject
152
- subject.error(error)
153
-
154
- // Advance enough to cover the retry attempts (exponential backoff: 1s, 2s, 4s)
155
- for (let i = 0; i < 3; i++) {
156
- const delay = Math.pow(2, i) * 1000
157
- await vi.advanceTimersByTimeAsync(delay)
158
- }
165
+ subject.error(new Error('Query failed'))
159
166
 
160
- // Verify error was logged at least once during retries
161
- expect(consoleErrorSpy).toHaveBeenCalledWith(
162
- '[releases] Error in subscription:',
163
- error,
164
- 'Retry count:',
165
- expect.any(Number),
166
- )
167
+ await new Promise((resolve) => setTimeout(resolve, 0))
167
168
 
168
- // not sure how to test state.setError()
169
169
  expect(state.getCurrent()).toEqual(undefined)
170
-
171
- vi.useRealTimers()
172
170
  })
173
171
  })
@@ -1,15 +1,22 @@
1
- import {type SanityClient} from '@sanity/client'
2
1
  import {type SanityDocument} from '@sanity/types'
3
- import {catchError, EMPTY, retry, switchMap, timer} from 'rxjs'
2
+ import {map} from 'rxjs'
4
3
 
5
- import {getClientState} from '../client/clientStore'
4
+ /*
5
+ * Although this is an import dependency cycle, it is not a logical cycle:
6
+ * 1. releasesStore uses queryStore as a data source
7
+ * 2. queryStore calls getPerspectiveState for computing release perspectives
8
+ * 3. getPerspectiveState uses releasesStore as a data source
9
+ * 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
10
+ */
11
+ // eslint-disable-next-line import/no-cycle
12
+ import {getQueryState} from '../query/queryStore'
6
13
  import {bindActionByDataset, type BoundDatasetKey} from '../store/createActionBinder'
7
14
  import {createStateSourceAction} from '../store/createStateSourceAction'
8
15
  import {defineStore, type StoreContext} from '../store/defineStore'
9
- import {listenQuery} from '../utils/listenQuery'
10
16
  import {sortReleases} from './utils/sortReleases'
11
17
 
12
18
  const ARCHIVED_RELEASE_STATES = ['archived', 'published']
19
+ const STABLE_EMPTY_RELEASES: ReleaseDocument[] = []
13
20
 
14
21
  /**
15
22
  * Represents a document in a Sanity dataset that represents release options.
@@ -55,51 +62,30 @@ export const getActiveReleasesState = bindActionByDataset(
55
62
  )
56
63
 
57
64
  const RELEASES_QUERY = 'releases::all()'
58
- const QUERY_PARAMS = {}
59
65
 
60
66
  const subscribeToReleases = ({
61
67
  instance,
62
68
  state,
63
69
  key: {projectId, dataset},
64
70
  }: StoreContext<ReleasesStoreState, BoundDatasetKey>) => {
65
- return getClientState(instance, {
66
- apiVersion: '2025-04-10',
71
+ const {observable: releases$} = getQueryState<ReleaseDocument[]>(instance, {
72
+ query: RELEASES_QUERY,
67
73
  perspective: 'raw',
68
74
  projectId,
69
75
  dataset,
76
+ tag: 'releases',
70
77
  })
71
- .observable.pipe(
72
- switchMap((client: SanityClient) =>
73
- // releases are system documents, and are not supported by useQueryState
74
- listenQuery<ReleaseDocument[]>(client, RELEASES_QUERY, QUERY_PARAMS, {
75
- tag: 'releases-listener',
76
- throttleTime: 1000,
77
- transitions: ['update', 'appear', 'disappear'],
78
- }).pipe(
79
- retry({
80
- count: 3,
81
- delay: (error, retryCount) => {
82
- // eslint-disable-next-line no-console
83
- console.error('[releases] Error in subscription:', error, 'Retry count:', retryCount)
84
- return timer(Math.min(1000 * Math.pow(2, retryCount), 10000))
85
- },
86
- }),
87
- catchError((error) => {
88
- state.set('setError', {error})
89
- return EMPTY
90
- }),
91
- ),
92
- ),
93
- )
94
- .subscribe({
95
- next: (releases) => {
78
+ return releases$
79
+ .pipe(
80
+ map((releases) => {
96
81
  // logic here mirrors that of studio:
97
82
  // https://github.com/sanity-io/sanity/blob/156e8fa482703d99219f08da7bacb384517f1513/packages/sanity/src/core/releases/store/useActiveReleases.ts#L29
98
83
  state.set('setActiveReleases', {
99
- activeReleases: sortReleases(releases ?? [])
84
+ activeReleases: sortReleases(releases ?? STABLE_EMPTY_RELEASES)
100
85
  .filter((release) => !ARCHIVED_RELEASE_STATES.includes(release.state))
101
86
  .reverse(),
102
87
  })
103
- },
104
- })
88
+ }),
89
+ )
90
+ .subscribe({error: (error) => state.set('setError', {error})})
105
91
  }
@@ -0,0 +1,7 @@
1
+ import {type PerspectiveHandle, type ReleasePerspective} from '../../config/sanityConfig'
2
+
3
+ export const isReleasePerspective = (
4
+ perspective: PerspectiveHandle['perspective'],
5
+ ): perspective is ReleasePerspective => {
6
+ return typeof perspective === 'object' && perspective !== null && 'releaseName' in perspective
7
+ }