@sanity/sdk 2.5.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 (46) hide show
  1. package/dist/index.d.ts +429 -27
  2. package/dist/index.js +657 -266
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -3
  5. package/src/_exports/index.ts +18 -3
  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/client/clientStore.test.ts +45 -43
  18. package/src/client/clientStore.ts +23 -9
  19. package/src/config/loggingConfig.ts +149 -0
  20. package/src/config/sanityConfig.ts +82 -22
  21. package/src/projection/getProjectionState.ts +6 -5
  22. package/src/projection/projectionQuery.test.ts +38 -55
  23. package/src/projection/projectionQuery.ts +27 -31
  24. package/src/projection/projectionStore.test.ts +4 -4
  25. package/src/projection/projectionStore.ts +3 -2
  26. package/src/projection/resolveProjection.ts +2 -2
  27. package/src/projection/statusQuery.test.ts +35 -0
  28. package/src/projection/statusQuery.ts +71 -0
  29. package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
  30. package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
  31. package/src/projection/types.ts +12 -0
  32. package/src/projection/util.ts +0 -1
  33. package/src/query/queryStore.test.ts +64 -0
  34. package/src/query/queryStore.ts +33 -11
  35. package/src/releases/getPerspectiveState.test.ts +17 -14
  36. package/src/releases/getPerspectiveState.ts +58 -38
  37. package/src/releases/releasesStore.test.ts +59 -61
  38. package/src/releases/releasesStore.ts +21 -35
  39. package/src/releases/utils/isReleasePerspective.ts +7 -0
  40. package/src/store/createActionBinder.test.ts +211 -1
  41. package/src/store/createActionBinder.ts +102 -13
  42. package/src/store/createSanityInstance.test.ts +85 -1
  43. package/src/store/createSanityInstance.ts +55 -4
  44. package/src/utils/logger-usage-example.md +141 -0
  45. package/src/utils/logger.test.ts +757 -0
  46. package/src/utils/logger.ts +537 -0
@@ -3,7 +3,6 @@ import {Subject} from 'rxjs'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
5
  import {getAuthMethodState, getTokenState} from '../auth/authStore'
6
- import {canvasSource, datasetSource, mediaLibrarySource} from '../config/sanityConfig'
7
6
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
8
7
  import {getClient, getClientState} from './clientStore'
9
8
 
@@ -34,7 +33,10 @@ beforeEach(() => {
34
33
  vi.mocked(createClient).mockImplementation(
35
34
  (clientConfig) => ({config: () => clientConfig}) as SanityClient,
36
35
  )
37
- instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
36
+ instance = createSanityInstance({
37
+ projectId: 'test-project',
38
+ dataset: 'test-dataset',
39
+ })
38
40
  })
39
41
 
40
42
  afterEach(() => {
@@ -176,18 +178,15 @@ describe('clientStore', () => {
176
178
 
177
179
  describe('source handling', () => {
178
180
  it('should create client when source is provided', () => {
179
- const source = datasetSource('source-project', 'source-dataset')
180
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
181
+ const client = getClient(instance, {
182
+ apiVersion: '2024-11-12',
183
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
184
+ })
181
185
 
182
186
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
183
187
  expect.objectContaining({
184
- apiVersion: '2024-11-12',
185
- source: expect.objectContaining({
186
- __sanity_internal_sourceId: {
187
- projectId: 'source-project',
188
- dataset: 'source-dataset',
189
- },
190
- }),
188
+ 'apiVersion': '2024-11-12',
189
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
191
190
  }),
192
191
  )
193
192
  // Client should be projectless - no projectId/dataset in config
@@ -195,19 +194,16 @@ describe('clientStore', () => {
195
194
  expect(client.config()).not.toHaveProperty('dataset')
196
195
  expect(client.config()).toEqual(
197
196
  expect.objectContaining({
198
- source: expect.objectContaining({
199
- __sanity_internal_sourceId: {
200
- projectId: 'source-project',
201
- dataset: 'source-dataset',
202
- },
203
- }),
197
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
204
198
  }),
205
199
  )
206
200
  })
207
201
 
208
202
  it('should create resource when source has array sourceId and be projectless', () => {
209
- const source = mediaLibrarySource('media-lib-123')
210
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
203
+ const client = getClient(instance, {
204
+ apiVersion: '2024-11-12',
205
+ source: {mediaLibraryId: 'media-lib-123'},
206
+ })
211
207
 
212
208
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
213
209
  expect.objectContaining({
@@ -226,8 +222,10 @@ describe('clientStore', () => {
226
222
  })
227
223
 
228
224
  it('should create resource when canvas source is provided and be projectless', () => {
229
- const source = canvasSource('canvas-123')
230
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
225
+ const client = getClient(instance, {
226
+ apiVersion: '2024-11-12',
227
+ source: {canvasId: 'canvas-123'},
228
+ })
231
229
 
232
230
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
233
231
  expect.objectContaining({
@@ -246,30 +244,26 @@ describe('clientStore', () => {
246
244
  })
247
245
 
248
246
  it('should create projectless client when source is provided, ignoring instance config', () => {
249
- const source = datasetSource('source-project', 'source-dataset')
250
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
247
+ const client = getClient(instance, {
248
+ apiVersion: '2024-11-12',
249
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
250
+ })
251
251
 
252
252
  // Client should be projectless - source takes precedence, instance config is ignored
253
253
  expect(client.config()).not.toHaveProperty('projectId')
254
254
  expect(client.config()).not.toHaveProperty('dataset')
255
255
  expect(client.config()).toEqual(
256
256
  expect.objectContaining({
257
- source: expect.objectContaining({
258
- __sanity_internal_sourceId: {
259
- projectId: 'source-project',
260
- dataset: 'source-dataset',
261
- },
262
- }),
257
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
263
258
  }),
264
259
  )
265
260
  })
266
261
 
267
262
  it('should warn when both source and explicit projectId/dataset are provided', () => {
268
263
  const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
269
- const source = datasetSource('source-project', 'source-dataset')
270
264
  const client = getClient(instance, {
271
265
  apiVersion: '2024-11-12',
272
- source,
266
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
273
267
  projectId: 'explicit-project',
274
268
  dataset: 'explicit-dataset',
275
269
  })
@@ -284,13 +278,18 @@ describe('clientStore', () => {
284
278
  })
285
279
 
286
280
  it('should create different clients for different sources', () => {
287
- const source1 = datasetSource('project-1', 'dataset-1')
288
- const source2 = datasetSource('project-2', 'dataset-2')
289
- const source3 = mediaLibrarySource('media-lib-1')
290
-
291
- const client1 = getClient(instance, {apiVersion: '2024-11-12', source: source1})
292
- const client2 = getClient(instance, {apiVersion: '2024-11-12', source: source2})
293
- const client3 = getClient(instance, {apiVersion: '2024-11-12', source: source3})
281
+ const client1 = getClient(instance, {
282
+ apiVersion: '2024-11-12',
283
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
284
+ })
285
+ const client2 = getClient(instance, {
286
+ apiVersion: '2024-11-12',
287
+ source: {mediaLibraryId: 'media-lib-123'},
288
+ })
289
+ const client3 = getClient(instance, {
290
+ apiVersion: '2024-11-12',
291
+ source: {canvasId: 'canvas-123'},
292
+ })
294
293
 
295
294
  expect(client1).not.toBe(client2)
296
295
  expect(client2).not.toBe(client3)
@@ -299,11 +298,14 @@ describe('clientStore', () => {
299
298
  })
300
299
 
301
300
  it('should reuse clients with identical source configurations', () => {
302
- const source = datasetSource('same-project', 'same-dataset')
303
- const options = {apiVersion: '2024-11-12', source}
304
-
305
- const client1 = getClient(instance, options)
306
- const client2 = getClient(instance, options)
301
+ const client1 = getClient(instance, {
302
+ apiVersion: '2024-11-12',
303
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
304
+ })
305
+ const client2 = getClient(instance, {
306
+ apiVersion: '2024-11-12',
307
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
308
+ })
307
309
 
308
310
  expect(client1).toBe(client2)
309
311
  expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1)
@@ -2,7 +2,12 @@ import {type ClientConfig, createClient, type SanityClient} from '@sanity/client
2
2
  import {pick} from 'lodash-es'
3
3
 
4
4
  import {getAuthMethodState, getTokenState} from '../auth/authStore'
5
- import {type DocumentSource, SOURCE_ID} from '../config/sanityConfig'
5
+ import {
6
+ type DocumentSource,
7
+ isCanvasSource,
8
+ isDatasetSource,
9
+ isMediaLibrarySource,
10
+ } from '../config/sanityConfig'
6
11
  import {bindActionGlobally} from '../store/createActionBinder'
7
12
  import {createStateSourceAction} from '../store/createStateSourceAction'
8
13
  import {defineStore, type StoreContext} from '../store/defineStore'
@@ -61,6 +66,11 @@ export interface ClientStoreState {
61
66
  authMethod?: 'localstorage' | 'cookie'
62
67
  }
63
68
 
69
+ interface ClientResource {
70
+ type: 'dataset' | 'media-library' | 'canvas'
71
+ id: string
72
+ }
73
+
64
74
  /**
65
75
  * Options used when retrieving a client instance from the client store.
66
76
  *
@@ -170,13 +180,17 @@ export const getClient = bindActionGlobally(
170
180
 
171
181
  const tokenFromState = state.get().token
172
182
  const {clients, authMethod} = state.get()
173
- const hasSource = !!options.source
174
- let sourceId = options.source?.[SOURCE_ID]
175
183
 
176
- let resource
177
- if (Array.isArray(sourceId)) {
178
- resource = {type: sourceId[0], id: sourceId[1]}
179
- sourceId = undefined
184
+ let resource: ClientResource | undefined
185
+
186
+ if (options.source) {
187
+ if (isDatasetSource(options.source)) {
188
+ resource = {type: 'dataset', id: `${options.source.projectId}.${options.source.dataset}`}
189
+ } else if (isMediaLibrarySource(options.source)) {
190
+ resource = {type: 'media-library', id: options.source.mediaLibraryId}
191
+ } else if (isCanvasSource(options.source)) {
192
+ resource = {type: 'canvas', id: options.source.canvasId}
193
+ }
180
194
  }
181
195
 
182
196
  const projectId = options.projectId ?? instance.config.projectId
@@ -185,7 +199,7 @@ export const getClient = bindActionGlobally(
185
199
 
186
200
  const effectiveOptions: ClientOptions = {
187
201
  ...DEFAULT_CLIENT_CONFIG,
188
- ...((options.scope === 'global' || !projectId || hasSource) && {useProjectHostname: false}),
202
+ ...((options.scope === 'global' || !projectId || resource) && {useProjectHostname: false}),
189
203
  token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),
190
204
  ...options,
191
205
  ...(projectId && {projectId}),
@@ -197,7 +211,7 @@ export const getClient = bindActionGlobally(
197
211
  // When a source is provided, don't use projectId/dataset - the client should be "projectless"
198
212
  // The client code itself will ignore the non-source config, so we do this to prevent confusing the user.
199
213
  // (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)
200
- if (hasSource) {
214
+ if (resource) {
201
215
  if (options.projectId || options.dataset) {
202
216
  // eslint-disable-next-line no-console
203
217
  console.warn(
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Public API for configuring SDK logging
3
+ *
4
+ * @module loggingConfig
5
+ */
6
+
7
+ import {configureLogging as _configureLogging, type LoggerConfig} from '../utils/logger'
8
+
9
+ /**
10
+ * Configure logging for the Sanity SDK
11
+ *
12
+ * This function allows you to control what logs are output by the SDK,
13
+ * making it easier to debug issues in development or production.
14
+ *
15
+ * @remarks
16
+ * **Zero-Config via Environment Variable (Recommended):**
17
+ *
18
+ * The SDK automatically reads the `DEBUG` environment variable, making it
19
+ * easy to enable logging without code changes:
20
+ *
21
+ * ```bash
22
+ * # Enable all SDK logging at debug level
23
+ * DEBUG=sanity:* npm start
24
+ *
25
+ * # Enable specific namespaces
26
+ * DEBUG=sanity:auth,sanity:document npm start
27
+ *
28
+ * # Enable trace level for all namespaces
29
+ * DEBUG=sanity:trace:* npm start
30
+ *
31
+ * # Enable internal/maintainer logs
32
+ * DEBUG=sanity:*:internal npm start
33
+ * ```
34
+ *
35
+ * This matches the pattern used by Sanity CLI and Studio, making it familiar
36
+ * and easy for support teams to help troubleshoot issues.
37
+ *
38
+ * **Programmatic Configuration (Advanced):**
39
+ *
40
+ * For more control (custom handlers, dynamic configuration), call this function
41
+ * explicitly. Programmatic configuration overrides environment variables.
42
+ *
43
+ * **For Application Developers:**
44
+ * Use `info`, `warn`, or `error` levels to see high-level SDK activity
45
+ * without being overwhelmed by internal details.
46
+ *
47
+ * **For SDK Maintainers:**
48
+ * Use `debug` or `trace` levels with `internal: true` to see detailed
49
+ * information about store operations, RxJS streams, and state transitions.
50
+ *
51
+ * **Instance Context:**
52
+ * Logs automatically include instance information (projectId, dataset, instanceId)
53
+ * when available, making it easier to debug multi-instance scenarios:
54
+ * ```
55
+ * [INFO] [auth] [project:abc] [dataset:production] User logged in
56
+ * ```
57
+ *
58
+ * **Available Namespaces:**
59
+ * - `sdk` - SDK initialization, configuration, and lifecycle
60
+ * - `auth` - Authentication and authorization (when instrumented in the future)
61
+ * - And more as logging is added to modules
62
+ *
63
+ * @example Zero-config via environment variable (recommended for debugging)
64
+ * ```bash
65
+ * # Just set DEBUG and run your app - no code changes needed!
66
+ * DEBUG=sanity:* npm start
67
+ * ```
68
+ *
69
+ * @example Programmatic configuration (application developer)
70
+ * ```ts
71
+ * import {configureLogging} from '@sanity/sdk'
72
+ *
73
+ * // Log warnings and errors for auth and document operations
74
+ * configureLogging({
75
+ * level: 'warn',
76
+ * namespaces: ['auth', 'document']
77
+ * })
78
+ * ```
79
+ *
80
+ * @example Programmatic configuration (SDK maintainer)
81
+ * ```ts
82
+ * import {configureLogging} from '@sanity/sdk'
83
+ *
84
+ * // Enable all logs including internal traces
85
+ * configureLogging({
86
+ * level: 'trace',
87
+ * namespaces: ['*'],
88
+ * internal: true
89
+ * })
90
+ * ```
91
+ *
92
+ * @example Custom handler (for testing)
93
+ * ```ts
94
+ * import {configureLogging} from '@sanity/sdk'
95
+ *
96
+ * const logs: string[] = []
97
+ * configureLogging({
98
+ * level: 'info',
99
+ * namespaces: ['*'],
100
+ * handler: {
101
+ * error: (msg) => logs.push(msg),
102
+ * warn: (msg) => logs.push(msg),
103
+ * info: (msg) => logs.push(msg),
104
+ * debug: (msg) => logs.push(msg),
105
+ * trace: (msg) => logs.push(msg),
106
+ * }
107
+ * })
108
+ * ```
109
+ *
110
+ * @public
111
+ */
112
+ export function configureLogging(config: LoggerConfig): void {
113
+ _configureLogging(config)
114
+
115
+ // Always log configuration (bypasses namespace filtering)
116
+ // This ensures users see the message regardless of which namespaces they enable
117
+ const configLevel = config.level || 'warn'
118
+ const shouldLog = ['info', 'debug', 'trace'].includes(configLevel) || configLevel === 'warn'
119
+
120
+ if (shouldLog && config.handler?.info) {
121
+ config.handler.info(`[${new Date().toISOString()}] [INFO] [sdk] Logging configured`, {
122
+ level: configLevel,
123
+ namespaces: config.namespaces || [],
124
+ internal: config.internal || false,
125
+ source: 'programmatic',
126
+ })
127
+ } else if (shouldLog) {
128
+ // eslint-disable-next-line no-console
129
+ console.info(`[${new Date().toISOString()}] [INFO] [sdk] Logging configured`, {
130
+ level: configLevel,
131
+ namespaces: config.namespaces || [],
132
+ internal: config.internal || false,
133
+ source: 'programmatic',
134
+ })
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Re-export types for public API
140
+ * @public
141
+ */
142
+ export type {
143
+ InstanceContext,
144
+ LogContext,
145
+ Logger,
146
+ LoggerConfig,
147
+ LogLevel,
148
+ LogNamespace,
149
+ } from '../utils/logger'
@@ -2,6 +2,40 @@ import {type ClientPerspective, type StackablePerspective} from '@sanity/client'
2
2
 
3
3
  import {type AuthConfig} from './authConfig'
4
4
 
5
+ /**
6
+ * A minimal Observable-compatible interface for subscribing to token changes.
7
+ * Any object with a `subscribe` method that follows this contract will work,
8
+ * including RxJS Observables. This avoids coupling the SDK to a specific
9
+ * reactive library.
10
+ *
11
+ * @public
12
+ */
13
+ export interface TokenSource {
14
+ /** Subscribe to token emissions. Emits `null` when logged out. */
15
+ subscribe(observer: {next: (token: string | null) => void}): {unsubscribe(): void}
16
+ }
17
+
18
+ /**
19
+ * Studio-specific configuration for the SDK.
20
+ * When present, the SDK operates in studio mode and derives auth from the
21
+ * provided token source instead of discovering tokens independently.
22
+ *
23
+ * @public
24
+ */
25
+ export interface StudioConfig {
26
+ /** Reactive auth token source from the Studio's auth store. */
27
+ auth?: {
28
+ /**
29
+ * A reactive token source. The SDK subscribes and stays in sync — the
30
+ * Studio is the single authority for auth and handles token refresh.
31
+ *
32
+ * Optional because older Studios may not expose it. When absent, the
33
+ * SDK falls back to localStorage/cookie discovery.
34
+ */
35
+ token?: TokenSource
36
+ }
37
+ }
38
+
5
39
  /**
6
40
  * Represents the minimal configuration required to identify a Sanity project.
7
41
  * @public
@@ -31,6 +65,11 @@ export interface PerspectiveHandle {
31
65
  export interface DatasetHandle<TDataset extends string = string, TProjectId extends string = string>
32
66
  extends ProjectHandle<TProjectId>, PerspectiveHandle {
33
67
  dataset?: TDataset
68
+ /**
69
+ * @beta
70
+ * Explicit source object to use for this operation.
71
+ */
72
+ source?: DocumentSource
34
73
  }
35
74
 
36
75
  /**
@@ -79,51 +118,72 @@ export interface SanityConfig extends DatasetHandle, PerspectiveHandle {
79
118
  */
80
119
  auth?: AuthConfig
81
120
  /**
82
- * Studio mode configuration for use of the SDK in a Sanity Studio
83
- * @remarks Controls whether studio mode features are enabled
121
+ * Studio configuration provided by a Sanity Studio workspace.
122
+ * When present, the SDK operates in studio mode and derives auth from the
123
+ * workspace's reactive token source — no manual configuration needed.
124
+ *
125
+ * @remarks Typically set automatically by `SanityApp` when it detects an
126
+ * `SDKStudioContext` provider. Can also be set explicitly for programmatic use.
127
+ */
128
+ studio?: StudioConfig
129
+
130
+ /**
131
+ * Studio mode configuration for use of the SDK in a Sanity Studio.
132
+ * @remarks Controls whether studio mode features are enabled.
133
+ * @deprecated Use `studio` instead, which provides richer integration
134
+ * with the Studio's workspace (auth token sync, etc.).
84
135
  */
85
136
  studioMode?: {
86
137
  enabled: boolean
87
138
  }
88
- }
89
139
 
90
- export const SOURCE_ID = '__sanity_internal_sourceId'
140
+ /**
141
+ * @beta
142
+ * A list of named sources to use for this instance.
143
+ */
144
+ sources?: Record<string, DocumentSource>
145
+ }
91
146
 
92
147
  /**
93
148
  * A document source can be used for querying.
149
+ * This will soon be the default way to identify where you are querying from.
94
150
  *
95
151
  * @beta
96
- * @see datasetSource Construct a document source for a given projectId and dataset.
97
- * @see mediaLibrarySource Construct a document source for a mediaLibraryId.
98
- * @see canvasSource Construct a document source for a canvasId.
99
152
  */
100
- export type DocumentSource = {
101
- [SOURCE_ID]: ['media-library', string] | ['canvas', string] | {projectId: string; dataset: string}
102
- }
153
+ export type DocumentSource = DatasetSource | MediaLibrarySource | CanvasSource
103
154
 
104
155
  /**
105
- * Returns a document source for a projectId and dataset.
106
- *
107
156
  * @beta
108
157
  */
109
- export function datasetSource(projectId: string, dataset: string): DocumentSource {
110
- return {[SOURCE_ID]: {projectId, dataset}}
158
+ export type DatasetSource = {projectId: string; dataset: string}
159
+
160
+ /**
161
+ * @beta
162
+ */
163
+ export type MediaLibrarySource = {mediaLibraryId: string}
164
+
165
+ /**
166
+ * @beta
167
+ */
168
+ export type CanvasSource = {canvasId: string}
169
+
170
+ /**
171
+ * @beta
172
+ */
173
+ export function isDatasetSource(source: DocumentSource): source is DatasetSource {
174
+ return 'projectId' in source && 'dataset' in source
111
175
  }
112
176
 
113
177
  /**
114
- * Returns a document source for a Media Library.
115
- *
116
178
  * @beta
117
179
  */
118
- export function mediaLibrarySource(id: string): DocumentSource {
119
- return {[SOURCE_ID]: ['media-library', id]}
180
+ export function isMediaLibrarySource(source: DocumentSource): source is MediaLibrarySource {
181
+ return 'mediaLibraryId' in source
120
182
  }
121
183
 
122
184
  /**
123
- * Returns a document source for a Canvas.
124
- *
125
185
  * @beta
126
186
  */
127
- export function canvasSource(id: string): DocumentSource {
128
- return {[SOURCE_ID]: ['canvas', id]}
187
+ export function isCanvasSource(source: DocumentSource): source is CanvasSource {
188
+ return 'canvasId' in source
129
189
  }
@@ -1,8 +1,9 @@
1
+ import {DocumentId, getPublishedId} from '@sanity/id-utils'
1
2
  import {type SanityProjectionResult} from 'groq'
2
3
  import {omit} from 'lodash-es'
3
4
 
4
5
  import {type DocumentHandle} from '../config/sanityConfig'
5
- import {bindActionByDataset} from '../store/createActionBinder'
6
+ import {bindActionBySourceAndPerspective} from '../store/createActionBinder'
6
7
  import {type SanityInstance} from '../store/createSanityInstance'
7
8
  import {
8
9
  createStateSourceAction,
@@ -10,7 +11,7 @@ import {
10
11
  type StateSource,
11
12
  } from '../store/createStateSourceAction'
12
13
  import {hashString} from '../utils/hashString'
13
- import {getPublishedId, insecureRandomId} from '../utils/ids'
14
+ import {insecureRandomId} from '../utils/ids'
14
15
  import {projectionStore} from './projectionStore'
15
16
  import {type ProjectionStoreState, type ProjectionValuePending} from './types'
16
17
  import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
@@ -70,21 +71,21 @@ export function getProjectionState(
70
71
  /**
71
72
  * @beta
72
73
  */
73
- export const _getProjectionState = bindActionByDataset(
74
+ export const _getProjectionState = bindActionBySourceAndPerspective(
74
75
  projectionStore,
75
76
  createStateSourceAction({
76
77
  selector: (
77
78
  {state}: SelectorContext<ProjectionStoreState>,
78
79
  options: ProjectionOptions<string, string, string, string>,
79
80
  ): ProjectionValuePending<object> | undefined => {
80
- const documentId = getPublishedId(options.documentId)
81
+ const documentId = getPublishedId(DocumentId(options.documentId))
81
82
  const projectionHash = hashString(options.projection)
82
83
  return state.values[documentId]?.[projectionHash] ?? STABLE_EMPTY_PROJECTION
83
84
  },
84
85
  onSubscribe: ({state}, options: ProjectionOptions<string, string, string, string>) => {
85
86
  const {projection, ...docHandle} = options
86
87
  const subscriptionId = insecureRandomId()
87
- const documentId = getPublishedId(docHandle.documentId)
88
+ const documentId = getPublishedId(DocumentId(docHandle.documentId))
88
89
  const validProjection = validateProjection(projection)
89
90
  const projectionHash = hashString(validProjection)
90
91