@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.
- package/dist/index.d.ts +124 -13
- package/dist/index.js +468 -243
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/_exports/index.ts +3 -0
- package/src/auth/authMode.test.ts +56 -0
- package/src/auth/authMode.ts +71 -0
- package/src/auth/authStore.test.ts +85 -4
- package/src/auth/authStore.ts +63 -125
- package/src/auth/authStrategy.ts +39 -0
- package/src/auth/dashboardAuth.ts +132 -0
- package/src/auth/standaloneAuth.ts +109 -0
- package/src/auth/studioAuth.ts +217 -0
- package/src/auth/studioModeAuth.test.ts +43 -1
- package/src/auth/studioModeAuth.ts +10 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
- package/src/config/sanityConfig.ts +48 -7
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.test.ts +38 -55
- package/src/projection/projectionQuery.ts +27 -31
- package/src/projection/projectionStore.test.ts +4 -4
- package/src/projection/projectionStore.ts +3 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/statusQuery.test.ts +35 -0
- package/src/projection/statusQuery.ts +71 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
- package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
- package/src/projection/types.ts +12 -0
- package/src/projection/util.ts +0 -1
- package/src/query/queryStore.test.ts +64 -0
- package/src/query/queryStore.ts +30 -10
- package/src/releases/getPerspectiveState.test.ts +17 -14
- package/src/releases/getPerspectiveState.ts +58 -38
- package/src/releases/releasesStore.test.ts +59 -61
- package/src/releases/releasesStore.ts +21 -35
- package/src/releases/utils/isReleasePerspective.ts +7 -0
- package/src/store/createActionBinder.test.ts +211 -1
- package/src/store/createActionBinder.ts +95 -17
- 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 {
|
|
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('../
|
|
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(
|
|
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(['
|
|
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(['
|
|
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: ['
|
|
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(['
|
|
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(['
|
|
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(['
|
|
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(['
|
|
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
|
|
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 =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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 {
|
|
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('../
|
|
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.
|
|
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(
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
|
72
|
+
it('should update active releases state when the query emits new data', async () => {
|
|
72
73
|
const releasesSubject = new Subject<ReleaseDocument[]>()
|
|
73
|
-
vi.mocked(
|
|
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
|
|
116
|
-
// Configure
|
|
117
|
-
vi.mocked(
|
|
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
|
|
134
|
+
it('should handle null/undefined from the query by defaulting to empty array', async () => {
|
|
128
135
|
// Test null case
|
|
129
|
-
vi.mocked(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
+
import {map} from 'rxjs'
|
|
4
3
|
|
|
5
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|