@sanity/sdk 2.3.1 → 2.5.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 (55) hide show
  1. package/dist/index.d.ts +173 -105
  2. package/dist/index.js +354 -122
  3. package/dist/index.js.map +1 -1
  4. package/package.json +12 -11
  5. package/src/_exports/index.ts +30 -0
  6. package/src/agent/agentActions.test.ts +81 -0
  7. package/src/agent/agentActions.ts +139 -0
  8. package/src/auth/authStore.test.ts +13 -13
  9. package/src/auth/refreshStampedToken.test.ts +16 -16
  10. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +6 -6
  11. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -4
  12. package/src/auth/utils.ts +36 -0
  13. package/src/client/clientStore.test.ts +151 -0
  14. package/src/client/clientStore.ts +39 -1
  15. package/src/comlink/controller/actions/destroyController.test.ts +2 -2
  16. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +6 -6
  17. package/src/comlink/controller/actions/getOrCreateController.test.ts +5 -5
  18. package/src/comlink/controller/actions/getOrCreateController.ts +1 -1
  19. package/src/comlink/controller/actions/releaseChannel.test.ts +3 -2
  20. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -4
  21. package/src/comlink/node/actions/getOrCreateNode.test.ts +7 -7
  22. package/src/comlink/node/actions/releaseNode.test.ts +2 -2
  23. package/src/comlink/node/comlinkNodeStore.test.ts +4 -3
  24. package/src/config/sanityConfig.ts +49 -3
  25. package/src/document/actions.test.ts +34 -0
  26. package/src/document/actions.ts +31 -7
  27. package/src/document/applyDocumentActions.test.ts +9 -6
  28. package/src/document/applyDocumentActions.ts +9 -49
  29. package/src/document/documentStore.test.ts +148 -107
  30. package/src/document/documentStore.ts +40 -10
  31. package/src/document/permissions.test.ts +9 -9
  32. package/src/document/permissions.ts +17 -7
  33. package/src/document/processActions.test.ts +345 -0
  34. package/src/document/processActions.ts +185 -2
  35. package/src/document/reducers.ts +13 -6
  36. package/src/presence/presenceStore.ts +13 -7
  37. package/src/preview/previewStore.test.ts +10 -2
  38. package/src/preview/previewStore.ts +2 -1
  39. package/src/preview/subscribeToStateAndFetchBatches.test.ts +8 -5
  40. package/src/preview/subscribeToStateAndFetchBatches.ts +9 -3
  41. package/src/projection/projectionStore.test.ts +18 -2
  42. package/src/projection/projectionStore.ts +2 -1
  43. package/src/projection/subscribeToStateAndFetchBatches.test.ts +6 -5
  44. package/src/projection/subscribeToStateAndFetchBatches.ts +9 -3
  45. package/src/query/queryStore.ts +7 -4
  46. package/src/releases/getPerspectiveState.ts +2 -2
  47. package/src/releases/releasesStore.ts +10 -4
  48. package/src/store/createActionBinder.test.ts +8 -6
  49. package/src/store/createActionBinder.ts +50 -14
  50. package/src/store/createStateSourceAction.test.ts +12 -11
  51. package/src/store/createStateSourceAction.ts +6 -6
  52. package/src/store/createStoreInstance.test.ts +29 -16
  53. package/src/store/createStoreInstance.ts +6 -5
  54. package/src/store/defineStore.test.ts +1 -1
  55. package/src/store/defineStore.ts +12 -7
@@ -140,6 +140,52 @@ export function processActions({
140
140
  switch (action.type) {
141
141
  case 'document.create': {
142
142
  const documentId = getId(action.documentId)
143
+
144
+ if (action.liveEdit) {
145
+ // For liveEdit documents, create directly without draft/published logic
146
+ if (working[documentId]) {
147
+ throw new ActionError({
148
+ documentId,
149
+ transactionId,
150
+ message: `This document already exists.`,
151
+ })
152
+ }
153
+
154
+ const newDocBase = {_type: action.documentType, _id: documentId}
155
+ const newDocWorking = {_type: action.documentType, _id: documentId}
156
+ const mutations: Mutation[] = [{create: newDocWorking}]
157
+
158
+ base = processMutations({
159
+ documents: base,
160
+ transactionId,
161
+ mutations: [{create: newDocBase}],
162
+ timestamp,
163
+ })
164
+ working = processMutations({
165
+ documents: working,
166
+ transactionId,
167
+ mutations,
168
+ timestamp,
169
+ })
170
+
171
+ if (!checkGrant(grants.create, working[documentId] as SanityDocument)) {
172
+ throw new PermissionActionError({
173
+ documentId,
174
+ transactionId,
175
+ message: `You do not have permission to create document "${documentId}".`,
176
+ })
177
+ }
178
+
179
+ outgoingMutations.push(...mutations)
180
+ outgoingActions.push({
181
+ actionType: 'sanity.action.document.create',
182
+ publishedId: documentId,
183
+ attributes: newDocWorking,
184
+ })
185
+ continue
186
+ }
187
+
188
+ // Standard draft/published logic
143
189
  const draftId = getDraftId(documentId)
144
190
  const publishedId = getPublishedId(documentId)
145
191
 
@@ -152,8 +198,18 @@ export function processActions({
152
198
  }
153
199
 
154
200
  // Spread the (possibly undefined) published version directly.
155
- const newDocBase = {...base[publishedId], _type: action.documentType, _id: draftId}
156
- const newDocWorking = {...working[publishedId], _type: action.documentType, _id: draftId}
201
+ const newDocBase = {
202
+ ...base[publishedId],
203
+ _type: action.documentType,
204
+ _id: draftId,
205
+ ...action.initialValue,
206
+ }
207
+ const newDocWorking = {
208
+ ...working[publishedId],
209
+ _type: action.documentType,
210
+ _id: draftId,
211
+ ...action.initialValue,
212
+ }
157
213
  const mutations: Mutation[] = [{create: newDocWorking}]
158
214
 
159
215
  base = processMutations({
@@ -188,6 +244,39 @@ export function processActions({
188
244
 
189
245
  case 'document.delete': {
190
246
  const documentId = action.documentId
247
+
248
+ if (action.liveEdit) {
249
+ // For liveEdit documents, delete directly
250
+ if (!working[documentId]) {
251
+ throw new ActionError({
252
+ documentId,
253
+ transactionId,
254
+ message: 'The document you are trying to delete does not exist.',
255
+ })
256
+ }
257
+
258
+ if (!checkGrant(grants.update, working[documentId])) {
259
+ throw new PermissionActionError({
260
+ documentId,
261
+ transactionId,
262
+ message: `You do not have permission to delete this document.`,
263
+ })
264
+ }
265
+
266
+ const mutations: Mutation[] = [{delete: {id: documentId}}]
267
+
268
+ base = processMutations({documents: base, transactionId, mutations, timestamp})
269
+ working = processMutations({documents: working, transactionId, mutations, timestamp})
270
+
271
+ outgoingMutations.push(...mutations)
272
+ outgoingActions.push({
273
+ actionType: 'sanity.action.document.delete',
274
+ publishedId: documentId,
275
+ })
276
+ continue
277
+ }
278
+
279
+ // Standard draft/published logic
191
280
  const draftId = getDraftId(documentId)
192
281
  const publishedId = getPublishedId(documentId)
193
282
 
@@ -230,6 +319,16 @@ export function processActions({
230
319
 
231
320
  case 'document.discard': {
232
321
  const documentId = getId(action.documentId)
322
+
323
+ if (action.liveEdit) {
324
+ throw new ActionError({
325
+ documentId,
326
+ transactionId,
327
+ message: `Cannot discard changes for liveEdit document "${documentId}". LiveEdit documents do not support drafts.`,
328
+ })
329
+ }
330
+
331
+ // Standard draft/published logic
233
332
  const draftId = getDraftId(documentId)
234
333
  const mutations: Mutation[] = [{delete: {id: draftId}}]
235
334
 
@@ -262,6 +361,70 @@ export function processActions({
262
361
 
263
362
  case 'document.edit': {
264
363
  const documentId = getId(action.documentId)
364
+
365
+ if (action.liveEdit) {
366
+ // For liveEdit documents, edit directly without draft logic
367
+ const userPatches = action.patches?.map((patch) => ({patch: {id: documentId, ...patch}}))
368
+
369
+ // skip this action if there are no associated patches
370
+ if (!userPatches?.length) continue
371
+
372
+ if (!working[documentId] || !base[documentId]) {
373
+ throw new ActionError({
374
+ documentId,
375
+ transactionId,
376
+ message: `Cannot edit document because it does not exist.`,
377
+ })
378
+ }
379
+
380
+ const baseBefore = base[documentId] as SanityDocument
381
+ if (userPatches) {
382
+ base = processMutations({
383
+ documents: base,
384
+ transactionId,
385
+ mutations: userPatches,
386
+ timestamp,
387
+ })
388
+ }
389
+
390
+ const baseAfter = base[documentId] as SanityDocument
391
+ const patches = diffValue(baseBefore, baseAfter)
392
+
393
+ const workingBefore = working[documentId] as SanityDocument
394
+ if (!checkGrant(grants.update, workingBefore)) {
395
+ throw new PermissionActionError({
396
+ documentId,
397
+ transactionId,
398
+ message: `You do not have permission to edit document "${documentId}".`,
399
+ })
400
+ }
401
+
402
+ const workingMutations = patches.map((patch) => ({patch: {id: documentId, ...patch}}))
403
+
404
+ working = processMutations({
405
+ documents: working,
406
+ transactionId,
407
+ mutations: workingMutations,
408
+ timestamp,
409
+ })
410
+
411
+ outgoingMutations.push(...workingMutations)
412
+ outgoingActions.push(
413
+ ...patches.map(
414
+ (patch): HttpAction => ({
415
+ actionType: 'sanity.action.document.edit',
416
+ // Server requires draftId to have drafts. prefix for validation, even for liveEdit
417
+ draftId: getDraftId(documentId),
418
+ publishedId: documentId,
419
+ patch: patch as PatchOperations,
420
+ }),
421
+ ),
422
+ )
423
+
424
+ continue
425
+ }
426
+
427
+ // Standard draft/published logic
265
428
  const draftId = getDraftId(documentId)
266
429
  const publishedId = getPublishedId(documentId)
267
430
  const userPatches = action.patches?.map((patch) => ({patch: {id: draftId, ...patch}}))
@@ -352,6 +515,16 @@ export function processActions({
352
515
 
353
516
  case 'document.publish': {
354
517
  const documentId = getId(action.documentId)
518
+
519
+ if (action.liveEdit) {
520
+ throw new ActionError({
521
+ documentId,
522
+ transactionId,
523
+ message: `Cannot publish liveEdit document "${documentId}". LiveEdit documents do not support drafts or publishing.`,
524
+ })
525
+ }
526
+
527
+ // Standard draft/published logic
355
528
  const draftId = getDraftId(documentId)
356
529
  const publishedId = getPublishedId(documentId)
357
530
 
@@ -418,6 +591,16 @@ export function processActions({
418
591
 
419
592
  case 'document.unpublish': {
420
593
  const documentId = getId(action.documentId)
594
+
595
+ if (action.liveEdit) {
596
+ throw new ActionError({
597
+ documentId,
598
+ transactionId,
599
+ message: `Cannot unpublish liveEdit document "${documentId}". LiveEdit documents do not support drafts or publishing.`,
600
+ })
601
+ }
602
+
603
+ // Standard draft/published logic
421
604
  const draftId = getDraftId(documentId)
422
605
  const publishedId = getPublishedId(documentId)
423
606
 
@@ -20,6 +20,7 @@ export type SyncTransactionState = Pick<
20
20
 
21
21
  type ActionMap = {
22
22
  create: 'sanity.action.document.version.create'
23
+ createLiveEdit: 'sanity.action.document.create'
23
24
  discard: 'sanity.action.document.version.discard'
24
25
  unpublish: 'sanity.action.document.unpublish'
25
26
  delete: 'sanity.action.document.delete'
@@ -34,6 +35,7 @@ type OptimisticLock = {
34
35
 
35
36
  export type HttpAction =
36
37
  | {actionType: ActionMap['create']; publishedId: string; attributes: SanityDocumentLike}
38
+ | {actionType: ActionMap['createLiveEdit']; publishedId: string; attributes: SanityDocumentLike}
37
39
  | {actionType: ActionMap['discard']; versionId: string; purge?: boolean}
38
40
  | {actionType: ActionMap['unpublish']; draftId: string; publishedId: string}
39
41
  | {actionType: ActionMap['delete']; publishedId: string; includeDrafts?: string[]}
@@ -550,13 +552,19 @@ export function removeSubscriptionIdFromDocument(
550
552
  export function manageSubscriberIds(
551
553
  {state}: StoreContext<SyncTransactionState>,
552
554
  documentId: string | string[],
555
+ options?: {expandDraftPublished?: boolean},
553
556
  ): () => void {
557
+ const expandDraftPublished = options?.expandDraftPublished ?? true
554
558
  const documentIds = Array.from(
555
559
  new Set(
556
- (Array.isArray(documentId) ? documentId : [documentId]).flatMap((id) => [
557
- getPublishedId(id),
558
- getDraftId(id),
559
- ]),
560
+ expandDraftPublished
561
+ ? (Array.isArray(documentId) ? documentId : [documentId]).flatMap((id) => [
562
+ getPublishedId(id),
563
+ getDraftId(id),
564
+ ])
565
+ : Array.isArray(documentId)
566
+ ? documentId
567
+ : [documentId],
560
568
  ),
561
569
  )
562
570
  const subscriptionId = insecureRandomId()
@@ -579,8 +587,7 @@ export function manageSubscriberIds(
579
587
  }
580
588
  }
581
589
 
582
- export function getDocumentIdsFromActions(action: DocumentAction | DocumentAction[]): string[] {
583
- const actions = Array.isArray(action) ? action : [action]
590
+ export function getDocumentIdsFromActions(actions: DocumentAction[]): string[] {
584
591
  return Array.from(
585
592
  new Set(
586
593
  actions
@@ -4,7 +4,7 @@ import {combineLatest, distinctUntilChanged, filter, map, of, Subscription, swit
4
4
 
5
5
  import {getTokenState} from '../auth/authStore'
6
6
  import {getClient} from '../client/clientStore'
7
- import {bindActionByDataset} from '../store/createActionBinder'
7
+ import {bindActionByDataset, type BoundDatasetKey} from '../store/createActionBinder'
8
8
  import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
9
9
  import {defineStore, type StoreContext} from '../store/defineStore'
10
10
  import {type SanityUser} from '../users/types'
@@ -23,15 +23,21 @@ const getInitialState = (): PresenceStoreState => ({
23
23
  })
24
24
 
25
25
  /** @public */
26
- export const presenceStore = defineStore<PresenceStoreState>({
26
+ export const presenceStore = defineStore<PresenceStoreState, BoundDatasetKey>({
27
27
  name: 'presence',
28
28
  getInitialState,
29
- initialize: (context: StoreContext<PresenceStoreState>) => {
30
- const {instance, state} = context
29
+ initialize: (context: StoreContext<PresenceStoreState, BoundDatasetKey>) => {
30
+ const {
31
+ instance,
32
+ state,
33
+ key: {projectId, dataset},
34
+ } = context
31
35
  const sessionId = crypto.randomUUID()
32
36
 
33
37
  const client = getClient(instance, {
34
38
  apiVersion: '2022-06-30',
39
+ projectId,
40
+ dataset,
35
41
  })
36
42
 
37
43
  const token$ = getTokenState(instance).observable.pipe(distinctUntilChanged())
@@ -112,9 +118,9 @@ const selectPresence = createSelector(
112
118
  export const getPresence = bindActionByDataset(
113
119
  presenceStore,
114
120
  createStateSourceAction({
115
- selector: (context: SelectorContext<PresenceStoreState>): UserPresence[] =>
121
+ selector: (context: SelectorContext<PresenceStoreState>, _?): UserPresence[] =>
116
122
  selectPresence(context.state),
117
- onSubscribe: (context) => {
123
+ onSubscribe: (context: StoreContext<PresenceStoreState, BoundDatasetKey>, _?) => {
118
124
  const userIds$ = context.state.observable.pipe(
119
125
  map((state) =>
120
126
  Array.from(state.locations.values())
@@ -134,7 +140,7 @@ export const getPresence = bindActionByDataset(
134
140
  getUserState(context.instance, {
135
141
  userId,
136
142
  resourceType: 'project',
137
- projectId: context.instance.config.projectId,
143
+ projectId: context.key.projectId,
138
144
  }).pipe(filter((v): v is NonNullable<typeof v> => !!v)),
139
145
  )
140
146
  return combineLatest(userObservables)
@@ -18,9 +18,17 @@ describe('previewStore', () => {
18
18
 
19
19
  const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
20
20
 
21
- const {state, dispose} = createStoreInstance(instance, previewStore)
21
+ const {state, dispose} = createStoreInstance(
22
+ instance,
23
+ {name: 'p.d', projectId: 'p', dataset: 'd'},
24
+ previewStore,
25
+ )
22
26
 
23
- expect(subscribeToStateAndFetchBatches).toHaveBeenCalledWith({instance, state})
27
+ expect(subscribeToStateAndFetchBatches).toHaveBeenCalledWith({
28
+ instance,
29
+ state,
30
+ key: {name: 'p.d', projectId: 'p', dataset: 'd'},
31
+ })
24
32
 
25
33
  dispose()
26
34
  instance.dispose()
@@ -1,3 +1,4 @@
1
+ import {type BoundDatasetKey} from '../store/createActionBinder'
1
2
  import {defineStore} from '../store/defineStore'
2
3
  import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
3
4
 
@@ -79,7 +80,7 @@ export interface PreviewStoreState {
79
80
  subscriptions: {[TDocumentId in string]?: {[TSubscriptionId in string]?: true}}
80
81
  }
81
82
 
82
- export const previewStore = defineStore<PreviewStoreState>({
83
+ export const previewStore = defineStore<PreviewStoreState, BoundDatasetKey>({
83
84
  name: 'Preview',
84
85
  getInitialState() {
85
86
  return {
@@ -2,6 +2,7 @@ import {NEVER, Observable, type Observer} from 'rxjs'
2
2
  import {describe, expect, it, vi} from 'vitest'
3
3
 
4
4
  import {getQueryState, resolveQuery} from '../query/queryStore'
5
+ import {type BoundDatasetKey} from '../store/createActionBinder'
5
6
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
7
  import {type StateSource} from '../store/createStateSourceAction'
7
8
  import {createStoreState, type StoreState} from '../store/createStoreState'
@@ -14,6 +15,7 @@ vi.mock('../query/queryStore')
14
15
  describe('subscribeToStateAndFetchBatches', () => {
15
16
  let instance: SanityInstance
16
17
  let state: StoreState<PreviewStoreState>
18
+ let key: BoundDatasetKey
17
19
 
18
20
  beforeEach(() => {
19
21
  vi.clearAllMocks()
@@ -22,6 +24,7 @@ describe('subscribeToStateAndFetchBatches', () => {
22
24
  subscriptions: {},
23
25
  values: {},
24
26
  })
27
+ key = {name: 'test.test', projectId: 'test', dataset: 'test'}
25
28
 
26
29
  vi.mocked(getQueryState).mockReturnValue({
27
30
  getCurrent: () => undefined,
@@ -36,7 +39,7 @@ describe('subscribeToStateAndFetchBatches', () => {
36
39
  })
37
40
 
38
41
  it('batches rapid subscription changes into single requests', async () => {
39
- const subscription = subscribeToStateAndFetchBatches({instance, state})
42
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
40
43
 
41
44
  // Add multiple subscriptions rapidly
42
45
  state.set('addSubscription1', {
@@ -77,7 +80,7 @@ describe('subscribeToStateAndFetchBatches', () => {
77
80
  observable: new Observable(subscriber),
78
81
  } as StateSource<PreviewQueryResult[] | undefined>)
79
82
 
80
- const subscription = subscribeToStateAndFetchBatches({instance, state})
83
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
81
84
 
82
85
  expect(subscriber).not.toHaveBeenCalled()
83
86
 
@@ -126,7 +129,7 @@ describe('subscribeToStateAndFetchBatches', () => {
126
129
  subscriptions: {doc1: {sub1: true}},
127
130
  })
128
131
 
129
- const subscription = subscribeToStateAndFetchBatches({instance, state})
132
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
130
133
 
131
134
  // Add a subscription for a document already in the batch
132
135
  state.set('addSubscriptionAlreadyInBatch', (prev) => ({
@@ -152,7 +155,7 @@ describe('subscribeToStateAndFetchBatches', () => {
152
155
 
153
156
  it('cancels and restarts fetches when subscription set changes', async () => {
154
157
  const abortSpy = vi.spyOn(AbortController.prototype, 'abort')
155
- const subscription = subscribeToStateAndFetchBatches({instance, state})
158
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
156
159
 
157
160
  // Add initial subscription
158
161
  state.set('addSubscription1', {
@@ -182,7 +185,7 @@ describe('subscribeToStateAndFetchBatches', () => {
182
185
  observable: new Observable(subscriber),
183
186
  } as StateSource<PreviewQueryResult[] | undefined>)
184
187
 
185
- const subscription = subscribeToStateAndFetchBatches({instance, state})
188
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
186
189
 
187
190
  // Add a subscription
188
191
  state.set('addSubscription', {
@@ -15,6 +15,7 @@ import {
15
15
  } from 'rxjs'
16
16
 
17
17
  import {getQueryState, resolveQuery} from '../query/queryStore'
18
+ import {type BoundDatasetKey} from '../store/createActionBinder'
18
19
  import {type StoreContext} from '../store/defineStore'
19
20
  import {createPreviewQuery, processPreviewQuery} from './previewQuery'
20
21
  import {type PreviewQueryResult, type PreviewStoreState} from './previewStore'
@@ -28,7 +29,8 @@ const isSetEqual = <T>(a: Set<T>, b: Set<T>) =>
28
29
  export const subscribeToStateAndFetchBatches = ({
29
30
  state,
30
31
  instance,
31
- }: StoreContext<PreviewStoreState>): Subscription => {
32
+ key: {projectId, dataset},
33
+ }: StoreContext<PreviewStoreState, BoundDatasetKey>): Subscription => {
32
34
  const newSubscriberIds$ = state.observable.pipe(
33
35
  map(({subscriptions}) => new Set(Object.keys(subscriptions))),
34
36
  distinctUntilChanged(isSetEqual),
@@ -64,6 +66,8 @@ export const subscribeToStateAndFetchBatches = ({
64
66
  params,
65
67
  tag: PREVIEW_TAG,
66
68
  perspective: PREVIEW_PERSPECTIVE,
69
+ projectId,
70
+ dataset,
67
71
  })
68
72
  const source$ = defer(() => {
69
73
  if (getCurrent() === undefined) {
@@ -74,6 +78,8 @@ export const subscribeToStateAndFetchBatches = ({
74
78
  tag: PREVIEW_TAG,
75
79
  perspective: PREVIEW_PERSPECTIVE,
76
80
  signal: controller.signal,
81
+ projectId,
82
+ dataset,
77
83
  }),
78
84
  ).pipe(switchMap(() => observable))
79
85
  }
@@ -91,8 +97,8 @@ export const subscribeToStateAndFetchBatches = ({
91
97
  }),
92
98
  map(({ids, data}) => ({
93
99
  values: processPreviewQuery({
94
- projectId: instance.config.projectId!,
95
- dataset: instance.config.dataset!,
100
+ projectId,
101
+ dataset,
96
102
  ids,
97
103
  results: data,
98
104
  }),
@@ -26,10 +26,26 @@ describe('projectionStore', () => {
26
26
 
27
27
  const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
28
28
 
29
- const {state, dispose} = createStoreInstance(instance, projectionStore)
29
+ const {state, dispose} = createStoreInstance(
30
+ instance,
31
+ {
32
+ name: 'p.d',
33
+ projectId: 'p',
34
+ dataset: 'd',
35
+ },
36
+ projectionStore,
37
+ )
30
38
 
31
39
  expect(subscribeToStateAndFetchBatches).toHaveBeenCalledOnce()
32
- expect(subscribeToStateAndFetchBatches).toHaveBeenCalledWith({instance, state})
40
+ expect(subscribeToStateAndFetchBatches).toHaveBeenCalledWith({
41
+ instance,
42
+ state,
43
+ key: {
44
+ name: 'p.d',
45
+ projectId: 'p',
46
+ dataset: 'd',
47
+ },
48
+ })
33
49
 
34
50
  dispose()
35
51
  instance.dispose()
@@ -1,8 +1,9 @@
1
+ import {type BoundDatasetKey} from '../store/createActionBinder'
1
2
  import {defineStore} from '../store/defineStore'
2
3
  import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
3
4
  import {type ProjectionStoreState} from './types'
4
5
 
5
- export const projectionStore = defineStore<ProjectionStoreState>({
6
+ export const projectionStore = defineStore<ProjectionStoreState, BoundDatasetKey>({
6
7
  name: 'Projection',
7
8
  getInitialState() {
8
9
  return {
@@ -15,6 +15,7 @@ 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
19
 
19
20
  beforeEach(() => {
20
21
  vi.clearAllMocks()
@@ -38,7 +39,7 @@ describe('subscribeToStateAndFetchBatches', () => {
38
39
  })
39
40
 
40
41
  it('batches rapid subscription changes into single requests', async () => {
41
- const subscription = subscribeToStateAndFetchBatches({instance, state})
42
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
42
43
  const projection = '{title, description}'
43
44
  const projectionHash = hashString(projection)
44
45
 
@@ -95,7 +96,7 @@ describe('subscribeToStateAndFetchBatches', () => {
95
96
  observable: new Observable(subscriber),
96
97
  } as StateSource<ProjectionQueryResult[] | undefined>)
97
98
 
98
- const subscription = subscribeToStateAndFetchBatches({instance, state})
99
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
99
100
  const projection = '{title}'
100
101
  const projectionHash = hashString(projection)
101
102
 
@@ -166,7 +167,7 @@ describe('subscribeToStateAndFetchBatches', () => {
166
167
  subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
167
168
  })
168
169
 
169
- const subscription = subscribeToStateAndFetchBatches({instance, state})
170
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
170
171
 
171
172
  // Add another subscription for doc1 (same hash)
172
173
  state.set('addSubscriptionAlreadyInBatch', (prev) => ({
@@ -218,7 +219,7 @@ describe('subscribeToStateAndFetchBatches', () => {
218
219
 
219
220
  it('cancels and restarts fetches when subscription set changes', async () => {
220
221
  const abortSpy = vi.spyOn(AbortController.prototype, 'abort')
221
- const subscription = subscribeToStateAndFetchBatches({instance, state})
222
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
222
223
  const projection = '{title, description}'
223
224
  const projectionHash = hashString(projection)
224
225
  const projection2 = '{_id}' // Different projection
@@ -266,7 +267,7 @@ describe('subscribeToStateAndFetchBatches', () => {
266
267
  observable: new Observable(subscriber),
267
268
  } as StateSource<ProjectionQueryResult[] | undefined>)
268
269
 
269
- const subscription = subscribeToStateAndFetchBatches({instance, state})
270
+ const subscription = subscribeToStateAndFetchBatches({instance, state, key})
270
271
  const projection = '{title, description}'
271
272
  const projectionHash = hashString(projection)
272
273
 
@@ -17,6 +17,7 @@ import {
17
17
  } from 'rxjs'
18
18
 
19
19
  import {getQueryState, resolveQuery} from '../query/queryStore'
20
+ import {type BoundDatasetKey} from '../store/createActionBinder'
20
21
  import {type StoreContext} from '../store/defineStore'
21
22
  import {
22
23
  createProjectionQuery,
@@ -34,7 +35,8 @@ const isSetEqual = <T>(a: Set<T>, b: Set<T>) =>
34
35
  export const subscribeToStateAndFetchBatches = ({
35
36
  state,
36
37
  instance,
37
- }: StoreContext<ProjectionStoreState>): Subscription => {
38
+ key: {projectId, dataset},
39
+ }: StoreContext<ProjectionStoreState, BoundDatasetKey>): Subscription => {
38
40
  const documentProjections$ = state.observable.pipe(
39
41
  map((s) => s.documentProjections),
40
42
  distinctUntilChanged(isEqual),
@@ -94,6 +96,8 @@ export const subscribeToStateAndFetchBatches = ({
94
96
  const {getCurrent, observable} = getQueryState<ProjectionQueryResult[]>(instance, {
95
97
  query,
96
98
  params,
99
+ projectId,
100
+ dataset,
97
101
  tag: PROJECTION_TAG,
98
102
  perspective: PROJECTION_PERSPECTIVE,
99
103
  })
@@ -104,6 +108,8 @@ export const subscribeToStateAndFetchBatches = ({
104
108
  resolveQuery<ProjectionQueryResult[]>(instance, {
105
109
  query,
106
110
  params,
111
+ projectId,
112
+ dataset,
107
113
  tag: PROJECTION_TAG,
108
114
  perspective: PROJECTION_PERSPECTIVE,
109
115
  signal: controller.signal,
@@ -125,8 +131,8 @@ export const subscribeToStateAndFetchBatches = ({
125
131
  }),
126
132
  map(({ids, data}) =>
127
133
  processProjectionQuery({
128
- projectId: instance.config.projectId!,
129
- dataset: instance.config.dataset!,
134
+ projectId,
135
+ dataset,
130
136
  ids,
131
137
  results: data,
132
138
  }),
@@ -23,9 +23,9 @@ import {
23
23
  } from 'rxjs'
24
24
 
25
25
  import {getClientState} from '../client/clientStore'
26
- import {type DatasetHandle} from '../config/sanityConfig'
26
+ import {type DatasetHandle, type DocumentSource} from '../config/sanityConfig'
27
27
  import {getPerspectiveState} from '../releases/getPerspectiveState'
28
- import {bindActionByDataset} from '../store/createActionBinder'
28
+ import {bindActionBySource} from '../store/createActionBinder'
29
29
  import {type SanityInstance} from '../store/createSanityInstance'
30
30
  import {
31
31
  createStateSourceAction,
@@ -62,6 +62,7 @@ export interface QueryOptions<
62
62
  DatasetHandle<TDataset, TProjectId> {
63
63
  query: TQuery
64
64
  params?: Record<string, unknown>
65
+ source?: DocumentSource
65
66
  }
66
67
 
67
68
  /**
@@ -160,6 +161,7 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
160
161
  projectId,
161
162
  dataset,
162
163
  tag,
164
+ source,
163
165
  perspective: perspectiveFromOptions,
164
166
  ...restOptions
165
167
  } = parseQueryKey(group$.key)
@@ -172,6 +174,7 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
172
174
  apiVersion: QUERY_STORE_API_VERSION,
173
175
  projectId,
174
176
  dataset,
177
+ source,
175
178
  }).observable
176
179
 
177
180
  return combineLatest([lastLiveEventId$, client$, perspective$]).pipe(
@@ -290,7 +293,7 @@ export function getQueryState(
290
293
  ): ReturnType<typeof _getQueryState> {
291
294
  return _getQueryState(...args)
292
295
  }
293
- const _getQueryState = bindActionByDataset(
296
+ const _getQueryState = bindActionBySource(
294
297
  queryStore,
295
298
  createStateSourceAction({
296
299
  selector: ({state, instance}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
@@ -349,7 +352,7 @@ export function resolveQuery<TData>(
349
352
  export function resolveQuery(...args: Parameters<typeof _resolveQuery>): Promise<unknown> {
350
353
  return _resolveQuery(...args)
351
354
  }
352
- const _resolveQuery = bindActionByDataset(
355
+ const _resolveQuery = bindActionBySource(
353
356
  queryStore,
354
357
  ({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
355
358
  const normalized = normalizeOptionsWithPerspective(instance, options)