@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
@@ -15,13 +15,18 @@ vi.mock('../query/queryStore')
15
15
  describe('subscribeToStateAndFetchBatches', () => {
16
16
  let instance: SanityInstance
17
17
  let state: StoreState<ProjectionStoreState>
18
- const key = {name: 'test.test', projectId: 'test', dataset: 'test'}
18
+ const key = {
19
+ name: 'test.test:drafts',
20
+ source: {projectId: 'test', dataset: 'test'},
21
+ perspective: 'drafts' as const,
22
+ }
19
23
 
20
24
  beforeEach(() => {
21
25
  vi.clearAllMocks()
22
26
  instance = createSanityInstance({projectId: 'test', dataset: 'test'})
23
27
  state = createStoreState<ProjectionStoreState>({
24
28
  documentProjections: {},
29
+ documentStatuses: {},
25
30
  subscriptions: {},
26
31
  values: {},
27
32
  })
@@ -65,19 +70,14 @@ describe('subscribeToStateAndFetchBatches', () => {
65
70
  // Wait for debounce
66
71
  await new Promise((resolve) => setTimeout(resolve, 100))
67
72
 
68
- // Should still be 1 call because projections are identical
69
- expect(getQueryState).toHaveBeenCalledTimes(1)
73
+ // only 2 calls (one for projection, one for status) even though we added 2 subscriptions
74
+ expect(getQueryState).toHaveBeenCalledTimes(2)
70
75
  expect(getQueryState).toHaveBeenCalledWith(
71
76
  instance,
72
77
  expect.objectContaining({
73
- query: expect.any(String),
78
+ perspective: 'drafts',
74
79
  params: {
75
- [`__ids_${projectionHash}`]: expect.arrayContaining([
76
- 'doc1',
77
- 'drafts.doc1',
78
- 'doc2',
79
- 'drafts.doc2',
80
- ]),
80
+ [`__ids_${projectionHash}`]: expect.arrayContaining(['doc1', 'doc2']),
81
81
  },
82
82
  }),
83
83
  )
@@ -87,47 +87,43 @@ describe('subscribeToStateAndFetchBatches', () => {
87
87
 
88
88
  it('processes query results and updates state with resolved values', async () => {
89
89
  const teardown = vi.fn()
90
- const subscriber = vi
91
- .fn<(observer: Observer<ProjectionQueryResult[] | undefined>) => () => void>()
92
- .mockReturnValue(teardown)
93
-
94
- vi.mocked(getQueryState).mockReturnValue({
95
- getCurrent: () => undefined,
96
- observable: new Observable(subscriber),
97
- } as StateSource<ProjectionQueryResult[] | undefined>)
90
+ const projectionObservers: Observer<ProjectionQueryResult[] | undefined>[] = []
91
+ const statusObservers: Observer<{_id: string; _updatedAt: string}[] | undefined>[] = []
92
+
93
+ vi.mocked(getQueryState).mockImplementation((_, options) => {
94
+ const isStatusQuery = options.perspective === 'raw'
95
+ const observers = isStatusQuery ? statusObservers : projectionObservers
96
+ const observable = new Observable<unknown>((observer) => {
97
+ observers.push(observer as Observer<ProjectionQueryResult[] | undefined>)
98
+ return teardown
99
+ })
100
+ return {
101
+ getCurrent: () => undefined,
102
+ observable,
103
+ } as StateSource<ProjectionQueryResult[] | undefined>
104
+ })
98
105
 
99
106
  const subscription = subscribeToStateAndFetchBatches({instance, state, key})
100
107
  const projection = '{title}'
101
108
  const projectionHash = hashString(projection)
102
109
 
103
- expect(subscriber).not.toHaveBeenCalled()
104
-
105
110
  // Add a subscription
106
111
  state.set('addSubscription', {
107
112
  documentProjections: {doc1: {[projectionHash]: projection}},
108
113
  subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
109
114
  })
110
115
 
111
- expect(subscriber).not.toHaveBeenCalled()
112
-
113
- // Wait for debounce
116
+ // Wait for debounce (combineLatest subscribes to both projection and status)
114
117
  await new Promise((resolve) => setTimeout(resolve, 100))
115
118
 
116
- expect(subscriber).toHaveBeenCalled()
119
+ expect(projectionObservers.length).toBe(1)
120
+ expect(statusObservers.length).toBe(1)
117
121
  expect(teardown).not.toHaveBeenCalled()
118
122
 
119
- const [observer] = subscriber.mock.lastCall!
120
-
121
123
  const timestamp = new Date().toISOString()
122
124
 
123
- observer.next([
124
- {
125
- _id: 'doc1',
126
- _type: 'doc',
127
- _updatedAt: timestamp,
128
- result: {title: 'resolved'},
129
- __projectionHash: projectionHash,
130
- },
125
+ // Emit projection results first
126
+ projectionObservers[0]!.next([
131
127
  {
132
128
  _id: 'drafts.doc1',
133
129
  _type: 'doc',
@@ -137,6 +133,12 @@ describe('subscribeToStateAndFetchBatches', () => {
137
133
  },
138
134
  ])
139
135
 
136
+ // Emit status results (raw query returns _id, _updatedAt per document variant)
137
+ statusObservers[0]!.next([
138
+ {_id: 'doc1', _updatedAt: timestamp},
139
+ {_id: 'drafts.doc1', _updatedAt: timestamp},
140
+ ])
141
+
140
142
  const {values} = state.get()
141
143
  expect(values['doc1']?.[projectionHash]).toEqual({
142
144
  isPending: false,
@@ -247,11 +249,9 @@ describe('subscribeToStateAndFetchBatches', () => {
247
249
 
248
250
  await new Promise((resolve) => setTimeout(resolve, 100))
249
251
 
250
- // Expected calls:
251
- // 1. Initial fetch (doc1, hash1)
252
- // 2. Adding doc2 subscription (optimistic update, no fetch)
253
- // 3. Debounced fetch for (doc1, hash1) AND (doc2, hash2)
254
- expect(getQueryState).toHaveBeenCalledTimes(initialQueryCallCount + 1)
252
+ // Expected calls: initial batch has 2 (projection + status). After adding doc2,
253
+ // debounced fetch triggers 2 more (projection + status for new batch).
254
+ expect(getQueryState).toHaveBeenCalledTimes(initialQueryCallCount + 2)
255
255
  // Abort should have been called because the required projections changed
256
256
  expect(abortSpy).toHaveBeenCalled()
257
257
 
@@ -259,13 +259,21 @@ describe('subscribeToStateAndFetchBatches', () => {
259
259
  })
260
260
 
261
261
  it('processes and applies fetch results correctly', async () => {
262
- const subscriber =
263
- vi.fn<(observer: Observer<ProjectionQueryResult[] | undefined>) => () => void>()
264
-
265
- vi.mocked(getQueryState).mockReturnValue({
266
- getCurrent: () => undefined,
267
- observable: new Observable(subscriber),
268
- } as StateSource<ProjectionQueryResult[] | undefined>)
262
+ const projectionObservers: Observer<ProjectionQueryResult[] | undefined>[] = []
263
+ const statusObservers: Observer<{_id: string; _updatedAt: string}[] | undefined>[] = []
264
+
265
+ vi.mocked(getQueryState).mockImplementation((_, options) => {
266
+ const isStatusQuery = options.perspective === 'raw'
267
+ const observers = isStatusQuery ? statusObservers : projectionObservers
268
+ const observable = new Observable<unknown>((observer) => {
269
+ observers.push(observer as Observer<ProjectionQueryResult[] | undefined>)
270
+ return () => {}
271
+ })
272
+ return {
273
+ getCurrent: () => undefined,
274
+ observable,
275
+ } as StateSource<ProjectionQueryResult[] | undefined>
276
+ })
269
277
 
270
278
  const subscription = subscribeToStateAndFetchBatches({instance, state, key})
271
279
  const projection = '{title, description}'
@@ -280,12 +288,12 @@ describe('subscribeToStateAndFetchBatches', () => {
280
288
 
281
289
  await new Promise((resolve) => setTimeout(resolve, 100))
282
290
 
283
- expect(subscriber).toHaveBeenCalled()
284
- const [observer] = subscriber.mock.lastCall!
291
+ expect(projectionObservers.length).toBe(1)
292
+ expect(statusObservers.length).toBe(1)
285
293
 
286
294
  // Emit fetch results
287
295
  const timestamp = '2024-01-01T00:00:00Z'
288
- observer.next([
296
+ projectionObservers[0]!.next([
289
297
  {
290
298
  _id: 'doc1',
291
299
  _type: 'test',
@@ -294,13 +302,18 @@ describe('subscribeToStateAndFetchBatches', () => {
294
302
  __projectionHash: projectionHash,
295
303
  },
296
304
  ])
305
+ statusObservers[0]!.next([
306
+ {_id: 'doc1', _updatedAt: timestamp},
307
+ {_id: 'drafts.doc1', _updatedAt: timestamp},
308
+ ])
297
309
 
298
- // Check that the state was updated
310
+ // Check that the state was updated (status query provides both draft and published _updatedAt)
299
311
  expect(state.get().values['doc1']?.[projectionHash]).toEqual({
300
312
  data: expect.objectContaining({
301
313
  title: 'Test Document',
302
314
  description: 'Test Description',
303
315
  _status: {
316
+ lastEditedDraftAt: timestamp,
304
317
  lastEditedPublishedAt: timestamp,
305
318
  },
306
319
  }),
@@ -16,27 +16,34 @@ import {
16
16
  tap,
17
17
  } from 'rxjs'
18
18
 
19
+ import {isDatasetSource} from '../config/sanityConfig'
19
20
  import {getQueryState, resolveQuery} from '../query/queryStore'
20
- import {type BoundDatasetKey} from '../store/createActionBinder'
21
+ import {type BoundPerspectiveKey} from '../store/createActionBinder'
21
22
  import {type StoreContext} from '../store/defineStore'
22
23
  import {
23
24
  createProjectionQuery,
24
25
  processProjectionQuery,
25
26
  type ProjectionQueryResult,
26
27
  } from './projectionQuery'
28
+ import {buildStatusQueryIds, processStatusQueryResults} from './statusQuery'
27
29
  import {type ProjectionStoreState} from './types'
28
- import {PROJECTION_PERSPECTIVE, PROJECTION_TAG} from './util'
30
+ import {PROJECTION_TAG} from './util'
29
31
 
30
32
  const BATCH_DEBOUNCE_TIME = 50
31
33
 
32
34
  const isSetEqual = <T>(a: Set<T>, b: Set<T>) =>
33
35
  a.size === b.size && Array.from(a).every((i) => b.has(i))
34
36
 
37
+ interface StatusQueryResult {
38
+ _id: string
39
+ _updatedAt: string
40
+ }
41
+
35
42
  export const subscribeToStateAndFetchBatches = ({
36
43
  state,
37
44
  instance,
38
- key: {projectId, dataset},
39
- }: StoreContext<ProjectionStoreState, BoundDatasetKey>): Subscription => {
45
+ key: {source, perspective},
46
+ }: StoreContext<ProjectionStoreState, BoundPerspectiveKey>): Subscription => {
40
47
  const documentProjections$ = state.observable.pipe(
41
48
  map((s) => s.documentProjections),
42
49
  distinctUntilChanged(isEqual),
@@ -92,34 +99,43 @@ export const subscribeToStateAndFetchBatches = ({
92
99
  const {query, params} = createProjectionQuery(ids, documentProjections)
93
100
  const controller = new AbortController()
94
101
 
95
- return new Observable<ProjectionQueryResult[]>((observer) => {
102
+ // Build status query IDs and query
103
+ const statusQueryIds = buildStatusQueryIds(ids, perspective)
104
+ const statusQuery = `*[_id in $statusIds]{_id, _updatedAt}`
105
+ const statusParams = {statusIds: statusQueryIds}
106
+
107
+ const projectionQuery$ = new Observable<ProjectionQueryResult[]>((observer) => {
96
108
  const {getCurrent, observable} = getQueryState<ProjectionQueryResult[]>(instance, {
97
- query,
98
- params,
99
- projectId,
100
- dataset,
101
- tag: PROJECTION_TAG,
102
- perspective: PROJECTION_PERSPECTIVE,
109
+ ...{
110
+ query,
111
+ params,
112
+ tag: PROJECTION_TAG,
113
+ perspective,
114
+ },
115
+ // temporary guard here until we're ready for everything to be queried via global API
116
+ ...(source && !isDatasetSource(source) ? {source} : {}),
103
117
  })
104
118
 
105
- const source$ = defer(() => {
119
+ const querySource$ = defer(() => {
106
120
  if (getCurrent() === undefined) {
107
121
  return from(
108
122
  resolveQuery<ProjectionQueryResult[]>(instance, {
109
- query,
110
- params,
111
- projectId,
112
- dataset,
113
- tag: PROJECTION_TAG,
114
- perspective: PROJECTION_PERSPECTIVE,
115
- signal: controller.signal,
123
+ ...{
124
+ query,
125
+ params,
126
+ tag: PROJECTION_TAG,
127
+ signal: controller.signal,
128
+ perspective,
129
+ },
130
+ // temporary guard here until we're ready for everything to be queried via global API in v3
131
+ ...(source && !isDatasetSource(source) ? {source} : {}),
116
132
  }),
117
133
  ).pipe(switchMap(() => observable))
118
134
  }
119
135
  return observable
120
136
  }).pipe(filter((result): result is ProjectionQueryResult[] => result !== undefined))
121
137
 
122
- const subscription = source$.subscribe(observer)
138
+ const subscription = querySource$.subscribe(observer)
123
139
 
124
140
  return () => {
125
141
  if (!controller.signal.aborted) {
@@ -127,16 +143,79 @@ export const subscribeToStateAndFetchBatches = ({
127
143
  }
128
144
  subscription.unsubscribe()
129
145
  }
130
- }).pipe(map((data) => ({data, ids})))
146
+ })
147
+
148
+ const statusQuery$ = new Observable<StatusQueryResult[]>((observer) => {
149
+ const {getCurrent, observable} = getQueryState<StatusQueryResult[]>(instance, {
150
+ ...{
151
+ query: statusQuery,
152
+ params: statusParams,
153
+ tag: PROJECTION_TAG,
154
+ perspective: 'raw',
155
+ },
156
+ // temporary guard here until we're ready for everything to be queried via global API
157
+ ...(source && !isDatasetSource(source) ? {source} : {}),
158
+ })
159
+
160
+ const statusQuerySource$ = defer(() => {
161
+ if (getCurrent() === undefined) {
162
+ return from(
163
+ resolveQuery<StatusQueryResult[]>(instance, {
164
+ ...{
165
+ query: statusQuery,
166
+ params: statusParams,
167
+ tag: PROJECTION_TAG,
168
+ signal: controller.signal,
169
+ perspective: 'raw',
170
+ },
171
+ // temporary guard here until we're ready for everything to be queried via global API
172
+ ...(source && !isDatasetSource(source) ? {source} : {}),
173
+ }),
174
+ ).pipe(switchMap(() => observable))
175
+ }
176
+ return observable
177
+ }).pipe(filter((result): result is StatusQueryResult[] => result !== undefined))
178
+
179
+ const subscription = statusQuerySource$.subscribe(observer)
180
+
181
+ return () => {
182
+ subscription.unsubscribe()
183
+ }
184
+ })
185
+
186
+ // Combine both streams: emit whenever either has a new value (after both have
187
+ // emitted at least one defined value). This keeps reacting to query store updates.
188
+ return combineLatest([projectionQuery$, statusQuery$]).pipe(
189
+ filter(
190
+ (pair): pair is [ProjectionQueryResult[], StatusQueryResult[]] =>
191
+ pair[0] !== undefined && pair[1] !== undefined,
192
+ ),
193
+ map(([projection, status]) => ({
194
+ data: projection,
195
+ ids,
196
+ statusResults: status,
197
+ })),
198
+ )
131
199
  }),
132
- map(({ids, data}) =>
133
- processProjectionQuery({
134
- projectId,
135
- dataset,
200
+ map(({ids, data, statusResults}) => {
201
+ // Process status results into documentStatuses (same shape as _status returned to users)
202
+ const documentStatuses = processStatusQueryResults(statusResults)
203
+ state.set('updateStatuses', (prev) => ({
204
+ documentStatuses: {
205
+ ...prev.documentStatuses,
206
+ ...documentStatuses,
207
+ },
208
+ }))
209
+
210
+ // Assemble projection values with _status (buildStatusForDocument in statusQuery)
211
+ const currentState = state.get()
212
+ return processProjectionQuery({
136
213
  ids,
137
214
  results: data,
138
- }),
139
- ),
215
+ documentStatuses: currentState.documentStatuses,
216
+ perspective: perspective,
217
+ })
218
+ }),
140
219
  )
141
220
  .subscribe({
142
221
  next: (processedValues) => {
@@ -29,6 +29,11 @@ interface DocumentProjectionSubscriptions {
29
29
  }
30
30
  }
31
31
 
32
+ interface DocumentStatus {
33
+ lastEditedDraftAt?: string
34
+ lastEditedPublishedAt?: string
35
+ lastEditedVersionAt?: string
36
+ }
32
37
  export interface ProjectionStoreState<TValue extends object = object> {
33
38
  /**
34
39
  * A map of document IDs to their projection values, organized by projection hash
@@ -50,4 +55,11 @@ export interface ProjectionStoreState<TValue extends object = object> {
50
55
  subscriptions: {
51
56
  [documentId: string]: DocumentProjectionSubscriptions
52
57
  }
58
+
59
+ /**
60
+ * A map of document IDs to their status information (same shape as _status returned to users)
61
+ */
62
+ documentStatuses: {
63
+ [documentId: string]: DocumentStatus
64
+ }
53
65
  }
@@ -1,5 +1,4 @@
1
1
  export const PROJECTION_TAG = 'projection'
2
- export const PROJECTION_PERSPECTIVE = 'raw'
3
2
  export const PROJECTION_STATE_CLEAR_DELAY = 1000
4
3
 
5
4
  export const STABLE_EMPTY_PROJECTION = {
@@ -3,6 +3,7 @@ import {delay, filter, firstValueFrom, Observable, of, Subject} from 'rxjs'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
5
  import {getClientState} from '../client/clientStore'
6
+ import {isCanvasSource} from '../config/sanityConfig'
6
7
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
8
  import {type StateSource} from '../store/createStateSourceAction'
8
9
  import {getQueryState, resolveQuery} from './queryStore'
@@ -16,6 +17,23 @@ vi.mock('../client/clientStore', () => ({
16
17
  getClientState: vi.fn(),
17
18
  }))
18
19
 
20
+ // Avoid initializing releases store/perspectives in these tests to prevent
21
+ // side-effect queries (e.g. releases::all()) that would duplicate fetch calls
22
+ vi.mock('../releases/getPerspectiveState', async () => {
23
+ const actual = await vi.importActual('../releases/getPerspectiveState')
24
+ return {
25
+ ...actual,
26
+ getPerspectiveState: vi.fn(
27
+ (_instance, options?: {perspective?: unknown}) =>
28
+ ({
29
+ subscribe: () => () => {},
30
+ getCurrent: () => (options?.perspective ?? 'drafts') as unknown,
31
+ observable: of((options?.perspective ?? 'drafts') as unknown),
32
+ }) as unknown as StateSource<unknown>,
33
+ ),
34
+ }
35
+ })
36
+
19
37
  describe('queryStore', () => {
20
38
  let instance: SanityInstance
21
39
  let liveEvents: Subject<LiveEvent>
@@ -56,6 +74,7 @@ describe('queryStore', () => {
56
74
  })
57
75
 
58
76
  afterEach(() => {
77
+ vi.mocked(getClientState).mockClear()
59
78
  instance.dispose()
60
79
  })
61
80
 
@@ -428,4 +447,49 @@ describe('queryStore', () => {
428
447
 
429
448
  base.dispose()
430
449
  })
450
+
451
+ it('uses source from params when passed in query options (listenForNewSubscribersAndFetch)', async () => {
452
+ const query = '*[_type == "movie"]'
453
+ const mediaLibrarySource = {mediaLibraryId: 'ml123'}
454
+
455
+ const state = getQueryState(instance, {query, source: mediaLibrarySource})
456
+ const unsubscribe = state.subscribe()
457
+
458
+ await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
459
+
460
+ // Verify getClientState was called with the source from params in listenForNewSubscribersAndFetch
461
+ // This call includes projectId, dataset, and source
462
+ expect(getClientState).toHaveBeenCalledWith(
463
+ instance,
464
+ expect.objectContaining({
465
+ source: expect.objectContaining({
466
+ mediaLibraryId: 'ml123',
467
+ }),
468
+ }),
469
+ )
470
+
471
+ unsubscribe()
472
+ })
473
+
474
+ it('uses source from store context key when not a dataset source (listenToLiveClientAndSetLastLiveEventIds)', async () => {
475
+ const query = '*[_type == "movie"]'
476
+ const canvasSource = {canvasId: 'canvas456'}
477
+
478
+ const state = getQueryState(instance, {query, source: canvasSource})
479
+ const unsubscribe = state.subscribe()
480
+
481
+ await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
482
+
483
+ // Verify getClientState was called with the canvas source for live events
484
+ // The source is extracted from the store key and passed when it's not a dataset source
485
+ // This call only has apiVersion and source (no projectId/dataset)
486
+ const calls = vi.mocked(getClientState).mock.calls
487
+ const liveClientCall = calls.find(
488
+ ([_instance, options]) =>
489
+ isCanvasSource(options.source!) && options.source.canvasId === 'canvas456',
490
+ )
491
+ expect(liveClientCall).toBeDefined()
492
+
493
+ unsubscribe()
494
+ })
431
495
  })
@@ -14,6 +14,7 @@ import {
14
14
  mergeMap,
15
15
  NEVER,
16
16
  Observable,
17
+ of,
17
18
  pairwise,
18
19
  race,
19
20
  share,
@@ -23,9 +24,18 @@ import {
23
24
  } from 'rxjs'
24
25
 
25
26
  import {getClientState} from '../client/clientStore'
26
- import {type DatasetHandle, type DocumentSource} from '../config/sanityConfig'
27
+ import {type DatasetHandle, isDatasetSource} from '../config/sanityConfig'
28
+ /*
29
+ * Although this is an import dependency cycle, it is not a logical cycle:
30
+ * 1. queryStore uses getPerspectiveState when resolving release perspectives
31
+ * 2. getPerspectiveState uses releasesStore as a data source
32
+ * 3. releasesStore uses queryStore as a data source
33
+ * 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
34
+ */
35
+ // eslint-disable-next-line import/no-cycle
27
36
  import {getPerspectiveState} from '../releases/getPerspectiveState'
28
- import {bindActionBySource} from '../store/createActionBinder'
37
+ import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
38
+ import {bindActionBySource, type BoundSourceKey} from '../store/createActionBinder'
29
39
  import {type SanityInstance} from '../store/createSanityInstance'
30
40
  import {
31
41
  createStateSourceAction,
@@ -64,7 +74,6 @@ export interface QueryOptions<
64
74
  DatasetHandle<TDataset, TProjectId> {
65
75
  query: TQuery
66
76
  params?: Record<string, unknown>
67
- source?: DocumentSource
68
77
  }
69
78
 
70
79
  /**
@@ -107,7 +116,7 @@ function normalizeOptionsWithPerspective(
107
116
  }
108
117
  }
109
118
 
110
- const queryStore = defineStore<QueryStoreState>({
119
+ const queryStore = defineStore<QueryStoreState, BoundSourceKey>({
111
120
  name: 'QueryStore',
112
121
  getInitialState: () => ({queries: {}}),
113
122
  initialize(context) {
@@ -168,9 +177,13 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
168
177
  ...restOptions
169
178
  } = parseQueryKey(group$.key)
170
179
 
171
- const perspective$ = getPerspectiveState(instance, {
172
- perspective: perspectiveFromOptions,
173
- }).observable.pipe(filter(Boolean))
180
+ // Short-circuit perspective resolution for non-release perspectives to avoid
181
+ // touching the releases store (and its initialization) unnecessarily.
182
+ const perspective$ = isReleasePerspective(perspectiveFromOptions)
183
+ ? getPerspectiveState(instance, {
184
+ perspective: perspectiveFromOptions,
185
+ }).observable.pipe(filter(Boolean))
186
+ : of(perspectiveFromOptions ?? QUERY_STORE_DEFAULT_PERSPECTIVE)
174
187
 
175
188
  const client$ = getClientState(instance, {
176
189
  apiVersion: QUERY_STORE_API_VERSION,
@@ -179,8 +192,12 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
179
192
  source,
180
193
  }).observable
181
194
 
182
- return combineLatest([lastLiveEventId$, client$, perspective$]).pipe(
183
- switchMap(([lastLiveEventId, client, perspective]) =>
195
+ return combineLatest({
196
+ lastLiveEventId: lastLiveEventId$,
197
+ client: client$,
198
+ perspective: perspective$,
199
+ }).pipe(
200
+ switchMap(({lastLiveEventId, client, perspective}) =>
184
201
  client.observable.fetch(query, params, {
185
202
  ...restOptions,
186
203
  perspective,
@@ -208,9 +225,12 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
208
225
  const listenToLiveClientAndSetLastLiveEventIds = ({
209
226
  state,
210
227
  instance,
211
- }: StoreContext<QueryStoreState>) => {
228
+ key: {source},
229
+ }: StoreContext<QueryStoreState, BoundSourceKey>) => {
212
230
  const liveMessages$ = getClientState(instance, {
213
231
  apiVersion: QUERY_STORE_API_VERSION,
232
+ // temporary guard here until we're ready for everything to be queried via global api
233
+ ...(source && !isDatasetSource(source) ? {source} : {}),
214
234
  }).observable.pipe(
215
235
  switchMap((client) =>
216
236
  defer(() =>