@sanity/sdk 2.8.0 → 2.10.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 (111) hide show
  1. package/dist/_chunks-dts/utils.d.ts +2450 -0
  2. package/dist/_chunks-es/_internal.js +129 -0
  3. package/dist/_chunks-es/_internal.js.map +1 -0
  4. package/dist/_chunks-es/createGroqSearchFilter.js +1537 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  6. package/dist/_chunks-es/telemetryManager.js +87 -0
  7. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  8. package/dist/_chunks-es/version.js +7 -0
  9. package/dist/_chunks-es/version.js.map +1 -0
  10. package/dist/_exports/_internal.d.ts +64 -0
  11. package/dist/_exports/_internal.js +20 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +2 -2343
  14. package/dist/index.js +465 -1813
  15. package/dist/index.js.map +1 -1
  16. package/package.json +17 -12
  17. package/src/_exports/_internal.ts +14 -0
  18. package/src/_exports/index.ts +18 -1
  19. package/src/auth/authStore.test.ts +150 -1
  20. package/src/auth/authStore.ts +11 -11
  21. package/src/auth/dashboardAuth.ts +2 -2
  22. package/src/auth/handleAuthCallback.ts +9 -3
  23. package/src/auth/logout.test.ts +1 -1
  24. package/src/auth/logout.ts +1 -1
  25. package/src/auth/refreshStampedToken.test.ts +118 -1
  26. package/src/auth/refreshStampedToken.ts +3 -2
  27. package/src/auth/standaloneAuth.ts +9 -3
  28. package/src/auth/studioAuth.ts +34 -7
  29. package/src/auth/studioModeAuth.ts +2 -1
  30. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
  31. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
  32. package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
  33. package/src/auth/utils.ts +33 -0
  34. package/src/client/clientStore.test.ts +44 -30
  35. package/src/client/clientStore.ts +49 -48
  36. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  37. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  38. package/src/comlink/node/getNodeState.ts +2 -1
  39. package/src/config/sanityConfig.ts +78 -12
  40. package/src/document/actions.ts +18 -11
  41. package/src/document/applyDocumentActions.test.ts +7 -6
  42. package/src/document/applyDocumentActions.ts +10 -4
  43. package/src/document/documentStore.test.ts +542 -188
  44. package/src/document/documentStore.ts +142 -76
  45. package/src/document/events.ts +7 -2
  46. package/src/document/permissions.test.ts +18 -16
  47. package/src/document/permissions.ts +35 -11
  48. package/src/document/processActions.test.ts +359 -32
  49. package/src/document/processActions.ts +106 -78
  50. package/src/document/reducers.test.ts +117 -29
  51. package/src/document/reducers.ts +47 -40
  52. package/src/document/sharedListener.ts +16 -6
  53. package/src/document/util.ts +14 -0
  54. package/src/favorites/favorites.test.ts +9 -2
  55. package/src/presence/bifurTransport.test.ts +46 -6
  56. package/src/presence/bifurTransport.ts +19 -2
  57. package/src/presence/presenceStore.test.ts +96 -0
  58. package/src/presence/presenceStore.ts +96 -24
  59. package/src/preview/getPreviewState.test.ts +115 -98
  60. package/src/preview/getPreviewState.ts +38 -60
  61. package/src/preview/previewProjectionUtils.test.ts +179 -0
  62. package/src/preview/previewProjectionUtils.ts +93 -0
  63. package/src/preview/resolvePreview.test.ts +42 -25
  64. package/src/preview/resolvePreview.ts +33 -10
  65. package/src/preview/{previewStore.ts → types.ts} +8 -17
  66. package/src/projection/getProjectionState.test.ts +16 -16
  67. package/src/projection/getProjectionState.ts +6 -5
  68. package/src/projection/projectionQuery.ts +2 -3
  69. package/src/projection/projectionStore.test.ts +2 -2
  70. package/src/projection/resolveProjection.ts +2 -2
  71. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  72. package/src/projection/subscribeToStateAndFetchBatches.ts +12 -11
  73. package/src/projection/types.ts +1 -1
  74. package/src/query/queryStore.test.ts +12 -12
  75. package/src/query/queryStore.ts +12 -11
  76. package/src/query/reducers.ts +3 -3
  77. package/src/releases/getPerspectiveState.ts +7 -6
  78. package/src/releases/releasesStore.test.ts +20 -5
  79. package/src/releases/releasesStore.ts +20 -8
  80. package/src/store/createActionBinder.test.ts +31 -31
  81. package/src/store/createActionBinder.ts +43 -38
  82. package/src/store/createSanityInstance.ts +2 -3
  83. package/src/store/createStateSourceAction.test.ts +62 -0
  84. package/src/store/createStateSourceAction.ts +34 -39
  85. package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
  86. package/src/telemetry/devMode.test.ts +52 -0
  87. package/src/telemetry/devMode.ts +40 -0
  88. package/src/telemetry/initTelemetry.test.ts +225 -0
  89. package/src/telemetry/initTelemetry.ts +205 -0
  90. package/src/telemetry/telemetryManager.test.ts +263 -0
  91. package/src/telemetry/telemetryManager.ts +187 -0
  92. package/src/users/reducers.ts +3 -4
  93. package/src/users/usersStore.test.ts +1 -0
  94. package/src/users/usersStore.ts +5 -1
  95. package/src/utils/createFetcherStore.test.ts +6 -4
  96. package/src/utils/createFetcherStore.ts +8 -5
  97. package/src/utils/getStagingApiHost.test.ts +21 -0
  98. package/src/utils/getStagingApiHost.ts +14 -0
  99. package/src/utils/ids.test.ts +1 -29
  100. package/src/utils/ids.ts +0 -10
  101. package/src/utils/isImportError.test.ts +72 -0
  102. package/src/utils/isImportError.ts +34 -0
  103. package/src/utils/object.test.ts +95 -0
  104. package/src/utils/object.ts +142 -0
  105. package/src/utils/setCleanupTimeout.ts +24 -0
  106. package/src/preview/previewQuery.test.ts +0 -236
  107. package/src/preview/previewQuery.ts +0 -153
  108. package/src/preview/previewStore.test.ts +0 -36
  109. package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
  110. package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
  111. package/src/preview/util.ts +0 -13
@@ -0,0 +1,179 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {createSanityInstance} from '../store/createSanityInstance'
5
+ import {normalizeMedia, transformProjectionToPreview} from './previewProjectionUtils'
6
+ import {type PreviewQueryResult} from './types'
7
+
8
+ // Mock the getClient function
9
+ vi.mock('../client/clientStore', () => ({
10
+ getClient: vi.fn(),
11
+ }))
12
+
13
+ const mockClient = {
14
+ config: () => ({projectId: 'test-project', dataset: 'test-dataset'}),
15
+ } as unknown as SanityClient
16
+
17
+ describe('normalizeMedia', () => {
18
+ beforeEach(async () => {
19
+ vi.clearAllMocks()
20
+ })
21
+
22
+ it('returns null if media is null or undefined', () => {
23
+ expect(normalizeMedia(null, mockClient)).toBeNull()
24
+ expect(normalizeMedia(undefined, mockClient)).toBeNull()
25
+ })
26
+
27
+ it('returns null if media does not have a valid asset', () => {
28
+ const invalidMedia1 = {media: {_ref: 'image-abc123-200x200-png'}} // Missing `asset` property
29
+ const invalidMedia2 = {asset: {ref: 'image-abc123-200x200-png'}} // Incorrect property name `ref`
30
+ expect(normalizeMedia(invalidMedia1, mockClient)).toBeNull()
31
+ expect(normalizeMedia(invalidMedia2, mockClient)).toBeNull()
32
+ })
33
+
34
+ it('returns null if media is not an object', () => {
35
+ expect(normalizeMedia(123, mockClient)).toBeNull()
36
+ expect(normalizeMedia('invalid', mockClient)).toBeNull()
37
+ })
38
+
39
+ it('returns a normalized URL for valid image asset objects', () => {
40
+ const validMedia = {type: 'image-asset', _ref: 'image-abc123-200x200-png'}
41
+ const result = normalizeMedia(validMedia, mockClient)
42
+ expect(result).toEqual({
43
+ type: 'image-asset',
44
+ _ref: 'image-abc123-200x200-png',
45
+ url: 'https://cdn.sanity.io/images/test-project/test-dataset/abc123-200x200.png',
46
+ })
47
+ })
48
+
49
+ it('handles image assets with expected URL format', () => {
50
+ const media = {type: 'image-asset', _ref: 'image-xyz456-400x400-jpg'}
51
+ const result = normalizeMedia(media, mockClient)
52
+ expect(result).toEqual({
53
+ type: 'image-asset',
54
+ _ref: 'image-xyz456-400x400-jpg',
55
+ url: 'https://cdn.sanity.io/images/test-project/test-dataset/xyz456-400x400.jpg',
56
+ })
57
+ })
58
+ })
59
+
60
+ describe('transformProjectionToPreview', () => {
61
+ const instance = createSanityInstance({
62
+ projectId: 'test-project',
63
+ dataset: 'test-dataset',
64
+ })
65
+
66
+ beforeEach(async () => {
67
+ vi.clearAllMocks()
68
+
69
+ // Mock getClient to return our mock client
70
+ const {getClient} = await import('../client/clientStore')
71
+ vi.mocked(getClient).mockReturnValue(mockClient)
72
+ })
73
+
74
+ it('transforms projection result with title and subtitle', () => {
75
+ const projectionResult: PreviewQueryResult = {
76
+ _id: 'doc1',
77
+ _type: 'article',
78
+ _updatedAt: '2026-01-01',
79
+ titleCandidates: {title: 'My Title'},
80
+ subtitleCandidates: {description: 'My Description'},
81
+ media: null,
82
+ }
83
+
84
+ const result = transformProjectionToPreview(instance, projectionResult)
85
+
86
+ expect(result).toEqual({
87
+ title: 'My Title',
88
+ subtitle: 'My Description',
89
+ media: null,
90
+ })
91
+ })
92
+
93
+ it('uses fallback title when no title candidates exist', () => {
94
+ const projectionResult: PreviewQueryResult = {
95
+ _id: 'doc1',
96
+ _type: 'article',
97
+ _updatedAt: '2026-01-01',
98
+ titleCandidates: {},
99
+ subtitleCandidates: {},
100
+ media: null,
101
+ }
102
+
103
+ const result = transformProjectionToPreview(instance, projectionResult)
104
+
105
+ expect(result.title).toBe('article: doc1')
106
+ expect(result.subtitle).toBeUndefined()
107
+ })
108
+
109
+ it('transforms projection result with media', () => {
110
+ const projectionResult: PreviewQueryResult = {
111
+ _id: 'doc1',
112
+ _type: 'article',
113
+ _updatedAt: '2026-01-01',
114
+ titleCandidates: {title: 'My Title'},
115
+ subtitleCandidates: {},
116
+ media: {type: 'image-asset', _ref: 'image-abc123-200x200-png', url: ''},
117
+ }
118
+
119
+ const result = transformProjectionToPreview(instance, projectionResult)
120
+
121
+ expect(result).toEqual({
122
+ title: 'My Title',
123
+ subtitle: undefined,
124
+ media: {
125
+ type: 'image-asset',
126
+ _ref: 'image-abc123-200x200-png',
127
+ url: 'https://cdn.sanity.io/images/test-project/test-dataset/abc123-200x200.png',
128
+ },
129
+ })
130
+ })
131
+
132
+ it('includes status when provided', () => {
133
+ const projectionResult: PreviewQueryResult = {
134
+ _id: 'doc1',
135
+ _type: 'article',
136
+ _updatedAt: '2026-01-01',
137
+ titleCandidates: {title: 'My Title'},
138
+ subtitleCandidates: {},
139
+ media: null,
140
+ _status: {
141
+ lastEditedPublishedAt: '2026-01-01',
142
+ lastEditedDraftAt: '2026-01-02',
143
+ },
144
+ }
145
+
146
+ const result = transformProjectionToPreview(instance, projectionResult)
147
+
148
+ expect(result).toEqual({
149
+ title: 'My Title',
150
+ subtitle: undefined,
151
+ media: null,
152
+ _status: {
153
+ lastEditedPublishedAt: '2026-01-01',
154
+ lastEditedDraftAt: '2026-01-02',
155
+ },
156
+ })
157
+ })
158
+
159
+ it('calls getClient with the provided resource', async () => {
160
+ const projectionResult: PreviewQueryResult = {
161
+ _id: 'doc1',
162
+ _type: 'article',
163
+ _updatedAt: '2026-01-01',
164
+ titleCandidates: {title: 'My Title'},
165
+ subtitleCandidates: {},
166
+ media: null,
167
+ }
168
+
169
+ const resource = {mediaLibraryId: 'test-library'}
170
+
171
+ transformProjectionToPreview(instance, projectionResult, resource)
172
+
173
+ const {getClient} = await import('../client/clientStore')
174
+ expect(getClient).toHaveBeenCalledWith(instance, {
175
+ apiVersion: 'v2025-05-06',
176
+ resource,
177
+ })
178
+ })
179
+ })
@@ -0,0 +1,93 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {createImageUrlBuilder} from '@sanity/image-url'
3
+
4
+ import {getClient} from '../client/clientStore'
5
+ import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
6
+ import {type SanityInstance} from '../store/createSanityInstance'
7
+ import {isObject} from '../utils/object'
8
+ import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
9
+ import {type PreviewQueryResult, type PreviewValue} from './types'
10
+
11
+ const API_VERSION = 'v2025-05-06'
12
+
13
+ /**
14
+ * Checks if the provided value has `_ref` property that is a string and starts with `image-`
15
+ */
16
+ function hasImageRef<T>(value: unknown): value is T & {_ref: string} {
17
+ return isObject(value) && '_ref' in value && typeof (value as {_ref: unknown})._ref === 'string'
18
+ }
19
+
20
+ /**
21
+ * Normalizes a media asset to a preview value.
22
+ * Adds a url to a media asset reference using `@sanity/image-url`.
23
+ *
24
+ * @internal
25
+ */
26
+ export function normalizeMedia(media: unknown, client: SanityClient): PreviewValue['media'] {
27
+ if (!media) return null
28
+ if (!hasImageRef(media)) return null
29
+
30
+ const builder = createImageUrlBuilder(client)
31
+ const url = builder.image({_ref: media._ref}).url()
32
+
33
+ return {
34
+ type: 'image-asset',
35
+ _ref: media._ref,
36
+ url,
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Finds a single field value from a set of candidates based on a priority list of field names.
42
+ * Returns the first non-empty string value found from the candidates matching the priority list order.
43
+ *
44
+ * @internal
45
+ */
46
+ function findFirstDefined(
47
+ fieldsToSearch: string[],
48
+ candidates: Record<string, unknown>,
49
+ exclude?: unknown,
50
+ ): string | undefined {
51
+ if (!candidates) return undefined
52
+
53
+ for (const field of fieldsToSearch) {
54
+ const value = candidates[field]
55
+ if (typeof value === 'string' && value.trim() !== '' && value !== exclude) {
56
+ return value
57
+ }
58
+ }
59
+
60
+ return undefined
61
+ }
62
+
63
+ /**
64
+ * Transforms a projection result (with titleCandidates, subtitleCandidates, media)
65
+ * into a PreviewValue (with title, subtitle, media).
66
+ *
67
+ * @param projectionResult - The raw projection result from GROQ
68
+ * @param instance - The Sanity instance to use for client configuration
69
+ * @param resource - Data resource for the preview
70
+ * @internal
71
+ */
72
+ export function transformProjectionToPreview(
73
+ instance: SanityInstance,
74
+ projectionResult: PreviewQueryResult,
75
+ resource?: DocumentResource,
76
+ ): PreviewValue {
77
+ const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
78
+ const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
79
+
80
+ // Get a client for the resource (if provided) or use the instance config
81
+ const client = getClient(instance, {
82
+ apiVersion: API_VERSION,
83
+ // TODO: remove in v3 when we're ready for everything to be queried via resource
84
+ resource: resource && !isDatasetResource(resource) ? resource : undefined,
85
+ })
86
+
87
+ return {
88
+ title: String(title || `${projectionResult._type}: ${projectionResult._id}`),
89
+ subtitle: subtitle || undefined,
90
+ media: normalizeMedia(projectionResult.media, client),
91
+ ...(projectionResult._status && {_status: projectionResult._status}),
92
+ }
93
+ }
@@ -1,28 +1,17 @@
1
- import {of} from 'rxjs'
2
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
1
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
2
 
4
- import {createDocumentHandle} from '../config/handles'
3
+ import {resolveProjection} from '../projection/resolveProjection'
4
+ import {type ProjectionValuePending} from '../projection/types'
5
5
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
- import {type StateSource} from '../store/createStateSourceAction'
7
- import {getPreviewState} from './getPreviewState'
8
- import {type PreviewValue, type ValuePending} from './previewStore'
9
6
  import {resolvePreview} from './resolvePreview'
7
+ import {type PreviewQueryResult} from './types'
10
8
 
11
- vi.mock('./getPreviewState')
9
+ vi.mock('../projection/resolveProjection')
12
10
 
13
11
  describe('resolvePreview', () => {
14
12
  let instance: SanityInstance
15
-
16
13
  beforeEach(() => {
17
- vi.resetAllMocks()
18
- // Create a mock that returns the correct ValuePending type
19
- vi.mocked(getPreviewState).mockReturnValue({
20
- observable: of({
21
- data: {title: 'test'},
22
- isPending: false,
23
- } as ValuePending<PreviewValue>),
24
- } as StateSource<ValuePending<PreviewValue>>)
25
-
14
+ vi.clearAllMocks()
26
15
  instance = createSanityInstance({projectId: 'p', dataset: 'd'})
27
16
  })
28
17
 
@@ -30,18 +19,46 @@ describe('resolvePreview', () => {
30
19
  instance.dispose()
31
20
  })
32
21
 
33
- it('resolves a preview and returns the first emitted value with results', async () => {
34
- const docHandle = createDocumentHandle({
35
- documentId: 'doc123',
36
- documentType: 'movie',
22
+ it('resolves and transforms projection result to preview format', async () => {
23
+ const mockProjectionResult: PreviewQueryResult = {
24
+ _id: 'doc1',
25
+ _type: 'article',
26
+ _updatedAt: '2024-01-01',
27
+ titleCandidates: {title: 'Resolved Title'},
28
+ subtitleCandidates: {description: 'Resolved Description'},
29
+ media: null,
30
+ }
31
+
32
+ vi.mocked(resolveProjection).mockResolvedValue({
33
+ data: mockProjectionResult,
34
+ isPending: false,
35
+ } as ProjectionValuePending<PreviewQueryResult>)
36
+
37
+ const result = await resolvePreview(instance, {
38
+ documentId: 'doc1',
39
+ documentType: 'article',
37
40
  })
38
41
 
39
- const result = await resolvePreview(instance, docHandle)
42
+ expect(result.data).toEqual({
43
+ title: 'Resolved Title',
44
+ subtitle: 'Resolved Description',
45
+ media: null,
46
+ })
47
+ expect(result.isPending).toBe(false)
48
+ })
40
49
 
41
- expect(getPreviewState).toHaveBeenCalledWith(instance, docHandle)
42
- expect(result).toEqual({
43
- data: {title: 'test'},
50
+ it('returns null data when projection resolves with null', async () => {
51
+ vi.mocked(resolveProjection).mockResolvedValue({
52
+ data: null,
44
53
  isPending: false,
45
54
  })
55
+
56
+ const result = await resolvePreview(instance, {
57
+ documentId: 'doc1',
58
+ documentType: 'article',
59
+ })
60
+
61
+ expect(result.data).toBeNull()
62
+ expect(result.isPending).toBe(false)
46
63
  })
47
64
  })
@@ -1,20 +1,43 @@
1
- import {filter, firstValueFrom} from 'rxjs'
2
-
3
1
  import {type DocumentHandle} from '../config/sanityConfig'
4
- import {bindActionByDataset} from '../store/createActionBinder'
5
- import {getPreviewState} from './getPreviewState'
6
- import {previewStore} from './previewStore'
2
+ import {resolveProjection} from '../projection/resolveProjection'
3
+ import {type SanityInstance} from '../store/createSanityInstance'
4
+ import {PREVIEW_PROJECTION} from './previewConstants'
5
+ import {transformProjectionToPreview} from './previewProjectionUtils'
6
+ import {type PreviewQueryResult, type PreviewValue, type ValuePending} from './types'
7
7
 
8
8
  /**
9
9
  * @beta
10
+ * @deprecated This type is deprecated and will be removed in a future release.
10
11
  */
11
12
  export type ResolvePreviewOptions = DocumentHandle
12
13
 
13
14
  /**
14
15
  * @beta
16
+ * @deprecated This function is deprecated and will be removed in a future release.
15
17
  */
16
- export const resolvePreview = bindActionByDataset(
17
- previewStore,
18
- ({instance}, docHandle: ResolvePreviewOptions) =>
19
- firstValueFrom(getPreviewState(instance, docHandle).observable.pipe(filter((i) => !!i.data))),
20
- )
18
+ export async function resolvePreview(
19
+ instance: SanityInstance,
20
+ options: ResolvePreviewOptions,
21
+ ): Promise<ValuePending<PreviewValue>> {
22
+ // Resolve the projection
23
+ const projectionResult = await resolveProjection<PreviewQueryResult>(instance, {
24
+ ...options,
25
+ projection: PREVIEW_PROJECTION,
26
+ })
27
+
28
+ if (!projectionResult.data) {
29
+ return {data: null, isPending: projectionResult.isPending}
30
+ }
31
+
32
+ // Transform to preview format
33
+ const previewValue = transformProjectionToPreview(
34
+ instance,
35
+ projectionResult.data,
36
+ options.resource,
37
+ )
38
+
39
+ return {
40
+ data: previewValue,
41
+ isPending: projectionResult.isPending,
42
+ }
43
+ }
@@ -1,7 +1,9 @@
1
- import {type BoundDatasetKey} from '../store/createActionBinder'
2
- import {defineStore} from '../store/defineStore'
3
- import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
1
+ import {type DocumentStatus} from '../projection/types'
4
2
 
3
+ /**
4
+ *
5
+ * @internal
6
+ */
5
7
  export interface PreviewQueryResult {
6
8
  _id: string
7
9
  _type: string
@@ -9,6 +11,7 @@ export interface PreviewQueryResult {
9
11
  titleCandidates: Record<string, unknown>
10
12
  subtitleCandidates: Record<string, unknown>
11
13
  media?: PreviewMedia | null
14
+ _status?: DocumentStatus
12
15
  }
13
16
 
14
17
  /**
@@ -74,22 +77,10 @@ export type ValuePending<T> = {
74
77
 
75
78
  /**
76
79
  * @public
80
+ * @deprecated This interface is kept for backwards compatibility but is no longer used internally.
81
+ * Preview state is now stored in the projection store.
77
82
  */
78
83
  export interface PreviewStoreState {
79
84
  values: {[TDocumentId in string]?: ValuePending<PreviewValue>}
80
85
  subscriptions: {[TDocumentId in string]?: {[TSubscriptionId in string]?: true}}
81
86
  }
82
-
83
- export const previewStore = defineStore<PreviewStoreState, BoundDatasetKey>({
84
- name: 'Preview',
85
- getInitialState() {
86
- return {
87
- subscriptions: {},
88
- values: {},
89
- }
90
- },
91
- initialize: (context) => {
92
- const subscription = subscribeToStateAndFetchBatches(context)
93
- return () => subscription.unsubscribe
94
- },
95
- })
@@ -126,7 +126,7 @@ describe('getProjectionState', () => {
126
126
  expect(state.get().subscriptions).toEqual({})
127
127
  expect(state.get().documentProjections).toEqual({})
128
128
 
129
- const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID 3
129
+ const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID 2
130
130
  expect(state.get().subscriptions).toEqual({
131
131
  [docHandle.documentId]: {[hash1]: {testSubId_2: true}},
132
132
  })
@@ -134,7 +134,7 @@ describe('getProjectionState', () => {
134
134
  [docHandle.documentId]: {[hash1]: projection1},
135
135
  })
136
136
 
137
- const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID 4
137
+ const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID 3
138
138
  expect(state.get().subscriptions).toEqual({
139
139
  [docHandle.documentId]: {
140
140
  [hash1]: {testSubId_2: true},
@@ -148,10 +148,12 @@ describe('getProjectionState', () => {
148
148
  },
149
149
  })
150
150
 
151
- const unsubscribe3 = projectionState1.subscribe(vi.fn()) // Should use ID 5
151
+ // should share ID 2 from the first subscription -- we only care if ANY subscription exists
152
+ // so we can include the document / projection in a bulk fetch
153
+ const unsubscribe3 = projectionState1.subscribe(vi.fn())
152
154
  expect(state.get().subscriptions).toEqual({
153
155
  [docHandle.documentId]: {
154
- [hash1]: {testSubId_2: true, testSubId_4: true},
156
+ [hash1]: {testSubId_2: true},
155
157
  [hash2]: {testSubId_3: true},
156
158
  },
157
159
  })
@@ -165,23 +167,20 @@ describe('getProjectionState', () => {
165
167
  })
166
168
 
167
169
  // --- Test Unsubscribe ---
168
- unsubscribe1() // Unsubscribes ID 3
169
- expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({
170
- testSubId_2: true,
171
- testSubId_4: true,
172
- })
170
+ unsubscribe1() // Unsubscribes first subscription consumer
171
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_2: true})
173
172
  vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
174
- expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_4: true})
173
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_2: true})
175
174
  expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toEqual(projection1)
176
175
 
177
- unsubscribe3() // Unsubscribes ID 5
176
+ unsubscribe3() // Also unsubscibes first projection subscription, should be removed
178
177
  vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
179
178
  expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toBeUndefined()
180
179
  expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toBeUndefined()
181
180
  expect(state.get().subscriptions[docHandle.documentId]?.[hash2]).toEqual({testSubId_3: true})
182
181
  expect(state.get().documentProjections[docHandle.documentId]?.[hash2]).toEqual(projection2)
183
182
 
184
- unsubscribe2() // Unsubscribes ID 4
183
+ unsubscribe2() // Unsubscribes second projection subscription, should be removed
185
184
  vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
186
185
  expect(state.get().subscriptions[docHandle.documentId]).toBeUndefined()
187
186
  expect(state.get().documentProjections[docHandle.documentId]).toBeUndefined()
@@ -203,14 +202,14 @@ describe('getProjectionState', () => {
203
202
  }))
204
203
 
205
204
  const unsubscribe1 = projectionState.subscribe(vi.fn()) // Should use ID 2
206
- const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should use ID 3
205
+ const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should reuse ID 2
207
206
 
208
207
  expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
209
208
  data: initialData,
210
209
  isPending: true,
211
210
  })
212
211
 
213
- unsubscribe1() // Unsubscribes ID 2
212
+ unsubscribe1() // Unsubscribes first subscription consumer
214
213
  vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
215
214
  expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
216
215
  data: initialData,
@@ -219,9 +218,10 @@ describe('getProjectionState', () => {
219
218
  expect(Object.keys(state.get().subscriptions[docHandle.documentId]?.[hash] ?? {}).length).toBe(
220
219
  1,
221
220
  )
222
- expect(state.get().subscriptions[docHandle.documentId]?.[hash]).toEqual({testSubId_3: true})
221
+ // should still have the same subscription ID since we're not unsubscribing the second consumer
222
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash]).toEqual({testSubId_2: true})
223
223
 
224
- unsubscribe2() // Unsubscribes ID 3
224
+ unsubscribe2() // Unsubscribes second subscription consumer
225
225
  expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
226
226
  data: initialData,
227
227
  isPending: true,
@@ -1,9 +1,8 @@
1
1
  import {DocumentId, getPublishedId} from '@sanity/id-utils'
2
2
  import {type SanityProjectionResult} from 'groq'
3
- import {omit} from 'lodash-es'
4
3
 
5
4
  import {type DocumentHandle} from '../config/sanityConfig'
6
- import {bindActionBySourceAndPerspective} from '../store/createActionBinder'
5
+ import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
7
6
  import {type SanityInstance} from '../store/createSanityInstance'
8
7
  import {
9
8
  createStateSourceAction,
@@ -12,6 +11,8 @@ import {
12
11
  } from '../store/createStateSourceAction'
13
12
  import {hashString} from '../utils/hashString'
14
13
  import {insecureRandomId} from '../utils/ids'
14
+ import {omitProperty} from '../utils/object'
15
+ import {setCleanupTimeout} from '../utils/setCleanupTimeout'
15
16
  import {projectionStore} from './projectionStore'
16
17
  import {type ProjectionStoreState, type ProjectionValuePending} from './types'
17
18
  import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
@@ -71,7 +72,7 @@ export function getProjectionState(
71
72
  /**
72
73
  * @beta
73
74
  */
74
- export const _getProjectionState = bindActionBySourceAndPerspective(
75
+ export const _getProjectionState = bindActionByResourceAndPerspective(
75
76
  projectionStore,
76
77
  createStateSourceAction({
77
78
  selector: (
@@ -110,9 +111,9 @@ export const _getProjectionState = bindActionBySourceAndPerspective(
110
111
  }))
111
112
 
112
113
  return () => {
113
- setTimeout(() => {
114
+ setCleanupTimeout(() => {
114
115
  state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
115
- const documentSubscriptionsForHash = omit(
116
+ const documentSubscriptionsForHash = omitProperty(
116
117
  prev.subscriptions[documentId]?.[projectionHash],
117
118
  subscriptionId,
118
119
  )
@@ -1,8 +1,7 @@
1
1
  import {type ClientPerspective} from '@sanity/client'
2
- import {DocumentId} from '@sanity/id-utils'
2
+ import {DocumentId, getPublishedId} from '@sanity/id-utils'
3
3
 
4
4
  import {type ReleasePerspective} from '../config/sanityConfig'
5
- import {getPublishedId} from '../utils/ids'
6
5
  import {
7
6
  type DocumentProjections,
8
7
  type DocumentProjectionValues,
@@ -85,7 +84,7 @@ export function processProjectionQuery({
85
84
  } = {}
86
85
 
87
86
  for (const result of results) {
88
- const originalId = getPublishedId(result._id)
87
+ const originalId = getPublishedId(DocumentId(result._id))
89
88
  const hash = result.__projectionHash
90
89
 
91
90
  if (!ids.has(originalId)) continue
@@ -30,7 +30,7 @@ describe('projectionStore', () => {
30
30
  instance,
31
31
  {
32
32
  name: 'p.d',
33
- source: {projectId: 'p', dataset: 'd'},
33
+ resource: {projectId: 'p', dataset: 'd'},
34
34
  perspective: 'drafts',
35
35
  },
36
36
  projectionStore,
@@ -42,7 +42,7 @@ describe('projectionStore', () => {
42
42
  state,
43
43
  key: {
44
44
  name: 'p.d',
45
- source: {projectId: 'p', dataset: 'd'},
45
+ resource: {projectId: 'p', dataset: 'd'},
46
46
  perspective: 'drafts',
47
47
  },
48
48
  })
@@ -1,7 +1,7 @@
1
1
  import {type SanityProjectionResult} from 'groq'
2
2
  import {filter, firstValueFrom} from 'rxjs'
3
3
 
4
- import {bindActionBySourceAndPerspective} from '../store/createActionBinder'
4
+ import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
5
5
  import {type SanityInstance} from '../store/createSanityInstance'
6
6
  import {getProjectionState, type ProjectionOptions} from './getProjectionState'
7
7
  import {projectionStore} from './projectionStore'
@@ -38,7 +38,7 @@ export function resolveProjection(
38
38
  /**
39
39
  * @beta
40
40
  */
41
- const _resolveProjection = bindActionBySourceAndPerspective(
41
+ const _resolveProjection = bindActionByResourceAndPerspective(
42
42
  projectionStore,
43
43
  (
44
44
  {instance}: {instance: SanityInstance},
@@ -17,7 +17,7 @@ describe('subscribeToStateAndFetchBatches', () => {
17
17
  let state: StoreState<ProjectionStoreState>
18
18
  const key = {
19
19
  name: 'test.test:drafts',
20
- source: {projectId: 'test', dataset: 'test'},
20
+ resource: {projectId: 'test', dataset: 'test'},
21
21
  perspective: 'drafts' as const,
22
22
  }
23
23