@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2

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 (241) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +163 -0
  3. package/dist/_exports/_internal.d.ts.map +1 -0
  4. package/dist/_exports/_internal.js +332 -0
  5. package/dist/_exports/_internal.js.map +1 -0
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-CtE9bLHW.js +754 -0
  15. package/dist/authStore-CtE9bLHW.js.map +1 -0
  16. package/dist/authStore-DKFX8fn7.d.ts +3179 -0
  17. package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2332
  29. package/dist/index.js +4171 -4332
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-DPf9sN3V.js +1346 -0
  36. package/dist/queryStore-DPf9sN3V.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-Dq_qCkeI.js +127 -0
  41. package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
  42. package/dist/version-BChjsi4u.js +9 -0
  43. package/dist/version-BChjsi4u.js.map +1 -0
  44. package/package.json +47 -52
  45. package/src/_exports/_internal.ts +27 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +125 -64
  49. package/src/access/checkPermissions.test.ts +55 -0
  50. package/src/access/checkPermissions.ts +54 -0
  51. package/src/agent/agentActions.test.ts +16 -60
  52. package/src/agent/agentActions.ts +25 -38
  53. package/src/applications/applications.test-d.ts +88 -0
  54. package/src/applications/applications.test.ts +130 -0
  55. package/src/applications/applications.ts +367 -0
  56. package/src/auth/authLogger.ts +30 -0
  57. package/src/auth/authStore.test.ts +194 -13
  58. package/src/auth/authStore.ts +58 -26
  59. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  60. package/src/auth/getOrganizationVerificationState.ts +4 -4
  61. package/src/auth/handleAuthCallback.test.ts +35 -1
  62. package/src/auth/handleAuthCallback.ts +25 -6
  63. package/src/auth/logout.test.ts +75 -2
  64. package/src/auth/logout.ts +23 -4
  65. package/src/auth/refreshStampedToken.test.ts +219 -469
  66. package/src/auth/refreshStampedToken.ts +79 -195
  67. package/src/auth/studioAuth.ts +1 -1
  68. package/src/auth/studioModeAuth.ts +2 -1
  69. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  70. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  71. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  72. package/src/client/clientStore.test.ts +62 -37
  73. package/src/client/clientStore.ts +45 -75
  74. package/src/client/liveEvents.test.ts +151 -0
  75. package/src/client/liveEvents.ts +107 -0
  76. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  77. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  78. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  79. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  80. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  81. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  82. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  83. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  84. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  85. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  86. package/src/comlink/node/getNodeState.test.ts +1 -1
  87. package/src/comlink/node/getNodeState.ts +2 -1
  88. package/src/comments/addonDatasetStore.test.ts +215 -0
  89. package/src/comments/addonDatasetStore.ts +249 -0
  90. package/src/comments/buildCommentThreads.test.ts +83 -0
  91. package/src/comments/buildCommentThreads.ts +54 -0
  92. package/src/comments/commentActions.test.ts +540 -0
  93. package/src/comments/commentActions.ts +555 -0
  94. package/src/comments/commentFieldPath.test.ts +40 -0
  95. package/src/comments/commentFieldPath.ts +44 -0
  96. package/src/comments/commentsConstants.ts +80 -0
  97. package/src/comments/commentsStore.test.ts +419 -0
  98. package/src/comments/commentsStore.ts +483 -0
  99. package/src/comments/normalizeComment.test.ts +116 -0
  100. package/src/comments/normalizeComment.ts +45 -0
  101. package/src/comments/observeComments.test.ts +193 -0
  102. package/src/comments/observeComments.ts +113 -0
  103. package/src/comments/reducers.test.ts +277 -0
  104. package/src/comments/reducers.ts +316 -0
  105. package/src/comments/types.ts +249 -0
  106. package/src/comments/weakenReferences.test.ts +47 -0
  107. package/src/comments/weakenReferences.ts +29 -0
  108. package/src/config/__tests__/handles.test.ts +18 -12
  109. package/src/config/handles.ts +25 -7
  110. package/src/config/sanityConfig.ts +40 -82
  111. package/src/datasets/datasets.test.ts +2 -2
  112. package/src/datasets/datasets.ts +10 -4
  113. package/src/document/actions.test.ts +115 -33
  114. package/src/document/actions.ts +212 -15
  115. package/src/document/applyDocumentActions.test.ts +35 -13
  116. package/src/document/applyDocumentActions.ts +30 -9
  117. package/src/document/documentConstants.ts +15 -0
  118. package/src/document/documentStore.concurrency.test.ts +1306 -0
  119. package/src/document/documentStore.test.ts +753 -273
  120. package/src/document/documentStore.ts +197 -75
  121. package/src/document/events.test.ts +57 -2
  122. package/src/document/events.ts +82 -26
  123. package/src/document/listen.test.ts +56 -0
  124. package/src/document/listen.ts +74 -21
  125. package/src/document/listenerEventOperators.test.ts +311 -0
  126. package/src/document/listenerEventOperators.ts +217 -0
  127. package/src/document/patchOperations.test.ts +76 -2
  128. package/src/document/patchOperations.ts +60 -1
  129. package/src/document/permissions.test.ts +107 -60
  130. package/src/document/permissions.ts +44 -19
  131. package/src/document/processActions/create.ts +138 -0
  132. package/src/document/processActions/delete.ts +100 -0
  133. package/src/document/processActions/discard.ts +63 -0
  134. package/src/document/processActions/edit.ts +154 -0
  135. package/src/document/processActions/processActions.ts +218 -0
  136. package/src/document/processActions/publish.ts +120 -0
  137. package/src/document/processActions/releaseArchive.ts +77 -0
  138. package/src/document/processActions/releaseCreate.ts +59 -0
  139. package/src/document/processActions/releaseDelete.ts +65 -0
  140. package/src/document/processActions/releaseEdit.ts +37 -0
  141. package/src/document/processActions/releasePublish.ts +45 -0
  142. package/src/document/processActions/releaseSchedule.ts +87 -0
  143. package/src/document/processActions/releaseUtil.ts +31 -0
  144. package/src/document/processActions/shared.ts +308 -0
  145. package/src/document/processActions/unpublish.ts +85 -0
  146. package/src/document/processActions.test.ts +1129 -121
  147. package/src/document/reducers.test.ts +772 -64
  148. package/src/document/reducers.ts +205 -51
  149. package/src/document/resourceRules.test.ts +178 -0
  150. package/src/document/resourceRules.ts +117 -0
  151. package/src/document/sharedListener.test.ts +16 -14
  152. package/src/document/sharedListener.ts +24 -8
  153. package/src/document/util.test.ts +46 -0
  154. package/src/document/util.ts +50 -0
  155. package/src/favorites/favorites.test.ts +5 -6
  156. package/src/installations/installations.test-d.ts +73 -0
  157. package/src/installations/installations.test.ts +62 -0
  158. package/src/installations/installations.ts +206 -0
  159. package/src/organization/organization.test-d.ts +36 -0
  160. package/src/organization/organization.test.ts +138 -0
  161. package/src/organization/organization.ts +138 -0
  162. package/src/organizations/organizations.test-d.ts +56 -0
  163. package/src/organizations/organizations.test.ts +150 -0
  164. package/src/organizations/organizations.ts +107 -0
  165. package/src/presence/bifurTransport.test.ts +201 -9
  166. package/src/presence/bifurTransport.ts +98 -14
  167. package/src/presence/paths.test.ts +70 -0
  168. package/src/presence/paths.ts +41 -0
  169. package/src/presence/presenceStore.test.ts +915 -24
  170. package/src/presence/presenceStore.ts +576 -104
  171. package/src/presence/types.ts +175 -3
  172. package/src/preview/previewProjectionUtils.test.ts +9 -22
  173. package/src/preview/previewProjectionUtils.ts +7 -3
  174. package/src/preview/types.ts +0 -15
  175. package/src/project/project.test-d.ts +37 -0
  176. package/src/project/project.test.ts +157 -12
  177. package/src/project/project.ts +138 -25
  178. package/src/projection/getProjectionState.test.ts +18 -20
  179. package/src/projection/getProjectionState.ts +23 -3
  180. package/src/projection/projectionQuery.ts +5 -5
  181. package/src/projection/projectionStore.test.ts +1 -9
  182. package/src/projection/resolveProjection.test.ts +1 -2
  183. package/src/projection/resolveProjection.ts +16 -0
  184. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  185. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  186. package/src/projects/projects.test-d.ts +32 -0
  187. package/src/projects/projects.test.ts +108 -42
  188. package/src/projects/projects.ts +63 -27
  189. package/src/query/queryStore.test.ts +259 -123
  190. package/src/query/queryStore.ts +85 -69
  191. package/src/query/reducers.test.ts +0 -42
  192. package/src/query/reducers.ts +2 -18
  193. package/src/releases/getPerspectiveState.test.ts +17 -23
  194. package/src/releases/getPerspectiveState.ts +8 -20
  195. package/src/releases/observeReleases.test.ts +127 -0
  196. package/src/releases/observeReleases.ts +102 -0
  197. package/src/releases/releasesStore.test.ts +100 -53
  198. package/src/releases/releasesStore.ts +69 -44
  199. package/src/releases/utils/sortReleases.test.ts +38 -2
  200. package/src/releases/utils/sortReleases.ts +15 -10
  201. package/src/store/createActionBinder.test.ts +109 -90
  202. package/src/store/createActionBinder.ts +36 -28
  203. package/src/store/createSanityInstance.test.ts +15 -13
  204. package/src/store/createSanityInstance.ts +14 -2
  205. package/src/store/createStateSourceAction.test.ts +64 -2
  206. package/src/store/createStateSourceAction.ts +38 -43
  207. package/src/store/createStoreInstance.test.ts +4 -2
  208. package/src/store/fetcherStore.test-d.ts +121 -0
  209. package/src/store/fetcherStore.test.ts +299 -0
  210. package/src/store/fetcherStore.ts +801 -0
  211. package/src/telemetry/environment.test.ts +119 -0
  212. package/src/telemetry/environment.ts +92 -0
  213. package/src/telemetry/events.ts +42 -0
  214. package/src/telemetry/initTelemetry.test.ts +432 -0
  215. package/src/telemetry/initTelemetry.ts +218 -0
  216. package/src/telemetry/telemetryManager.test.ts +327 -0
  217. package/src/telemetry/telemetryManager.ts +199 -0
  218. package/src/users/reducers.test.ts +3 -1
  219. package/src/users/reducers.ts +5 -6
  220. package/src/users/types.ts +4 -4
  221. package/src/users/usersStore.test.ts +16 -12
  222. package/src/users/usersStore.ts +5 -1
  223. package/src/utils/buildQuery.test.ts +14 -0
  224. package/src/utils/buildQuery.ts +17 -0
  225. package/src/utils/createFetcherStore.test.ts +7 -5
  226. package/src/utils/createFetcherStore.ts +8 -5
  227. package/src/utils/getStagingApiHost.test.ts +21 -0
  228. package/src/utils/getStagingApiHost.ts +14 -0
  229. package/src/utils/ids.test.ts +1 -29
  230. package/src/utils/ids.ts +0 -10
  231. package/src/utils/includeShape.ts +36 -0
  232. package/src/utils/isImportError.test.ts +72 -0
  233. package/src/utils/isImportError.ts +34 -0
  234. package/src/utils/logger.test.ts +12 -0
  235. package/src/utils/logger.ts +8 -3
  236. package/src/utils/object.test.ts +95 -0
  237. package/src/utils/object.ts +142 -0
  238. package/src/utils/setCleanupTimeout.ts +37 -0
  239. package/src/document/processActions.ts +0 -707
  240. package/src/utils/defineIntent.test.ts +0 -477
  241. package/src/utils/defineIntent.ts +0 -244
@@ -1,8 +1,8 @@
1
- import {CorsOriginError, type ResponseQueryOptions} from '@sanity/client'
1
+ import {type ResponseQueryOptions} from '@sanity/client'
2
+ import {type SanityQueryResult} from 'groq'
2
3
  import {
3
4
  catchError,
4
5
  combineLatest,
5
- defer,
6
6
  distinctUntilChanged,
7
7
  EMPTY,
8
8
  filter,
@@ -23,15 +23,8 @@ import {
23
23
  } from 'rxjs'
24
24
 
25
25
  import {getClientState} from '../client/clientStore'
26
- import {type ResourceHandle} from '../config/sanityConfig'
27
- /*
28
- * Although this is an import dependency cycle, it is not a logical cycle:
29
- * 1. queryStore uses getPerspectiveState when resolving release perspectives
30
- * 2. getPerspectiveState uses releasesStore as a data source
31
- * 3. releasesStore uses queryStore as a data source
32
- * 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
33
- */
34
- // eslint-disable-next-line import/no-cycle
26
+ import {observeLiveEvents} from '../client/liveEvents'
27
+ import {type DatasetHandle} from '../config/sanityConfig'
35
28
  import {getPerspectiveState} from '../releases/getPerspectiveState'
36
29
  import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
37
30
  import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
@@ -44,6 +37,7 @@ import {
44
37
  import {type StoreState} from '../store/createStoreState'
45
38
  import {defineStore, type StoreContext} from '../store/defineStore'
46
39
  import {insecureRandomId} from '../utils/ids'
40
+ import {setCleanupTimeout} from '../utils/setCleanupTimeout'
47
41
  import {
48
42
  QUERY_STATE_CLEAR_DELAY,
49
43
  QUERY_STORE_API_VERSION,
@@ -51,8 +45,6 @@ import {
51
45
  } from './queryStoreConstants'
52
46
  import {
53
47
  addSubscriber,
54
- cancelQuery,
55
- initializeQuery,
56
48
  type QueryStoreState,
57
49
  removeSubscriber,
58
50
  setLastLiveEventId,
@@ -70,7 +62,7 @@ export interface QueryOptions<
70
62
  >
71
63
  extends
72
64
  Pick<ResponseQueryOptions, 'useCdn' | 'cache' | 'next' | 'cacheMode' | 'tag'>,
73
- ResourceHandle<TDataset, TProjectId> {
65
+ DatasetHandle<TDataset, TProjectId> {
74
66
  query: TQuery
75
67
  params?: Record<string, unknown>
76
68
  }
@@ -88,9 +80,10 @@ export interface ResolveQueryOptions<
88
80
 
89
81
  const EMPTY_ARRAY: never[] = []
90
82
 
91
- /** @beta */
92
- export const getQueryKey = (options: QueryOptions): string => JSON.stringify(options)
93
- /** @beta */
83
+ /** @internal */
84
+ export const getQueryKey = (instance: SanityInstance, options: QueryOptions): string =>
85
+ JSON.stringify(normalizeOptionsWithPerspective(instance, options))
86
+ /** @internal */
94
87
  export const parseQueryKey = (key: string): QueryOptions => JSON.parse(key)
95
88
 
96
89
  /**
@@ -136,11 +129,7 @@ const errorHandler = (state: StoreState<{error?: unknown}>) => {
136
129
  return (error: unknown): void => state.set('setError', {error})
137
130
  }
138
131
 
139
- const listenForNewSubscribersAndFetch = ({
140
- state,
141
- instance,
142
- key: {resource: boundResource},
143
- }: StoreContext<QueryStoreState, BoundResourceKey>) => {
132
+ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QueryStoreState>) => {
144
133
  return state.observable
145
134
  .pipe(
146
135
  map((s) => new Set(Object.keys(s.queries))),
@@ -172,6 +161,8 @@ const listenForNewSubscribersAndFetch = ({
172
161
  const {
173
162
  query,
174
163
  params,
164
+ projectId,
165
+ dataset,
175
166
  tag,
176
167
  resource,
177
168
  perspective: perspectiveFromOptions,
@@ -183,19 +174,15 @@ const listenForNewSubscribersAndFetch = ({
183
174
  const perspective$ = isReleasePerspective(perspectiveFromOptions)
184
175
  ? getPerspectiveState(instance, {
185
176
  perspective: perspectiveFromOptions,
186
- resource: resource ?? boundResource,
177
+ resource,
187
178
  }).observable.pipe(filter(Boolean))
188
179
  : of(perspectiveFromOptions ?? QUERY_STORE_DEFAULT_PERSPECTIVE)
189
180
 
190
- // Use the store's bound resource as fallback when the query key
191
- // doesn't include an explicit resource. The store is scoped to a
192
- // specific resource via bindActionByResource, but the captured
193
- // `instance` may have a different default resource (e.g. when the
194
- // store was first created by a caller that passed an explicit
195
- // resource while using the root app instance).
196
181
  const client$ = getClientState(instance, {
197
182
  apiVersion: QUERY_STORE_API_VERSION,
198
- resource: resource ?? boundResource,
183
+ projectId,
184
+ dataset,
185
+ resource,
199
186
  }).observable
200
187
 
201
188
  return combineLatest({
@@ -213,15 +200,19 @@ const listenForNewSubscribersAndFetch = ({
213
200
  tag,
214
201
  }),
215
202
  ),
203
+ tap(({result, syncTags}) => {
204
+ state.set('setQueryData', setQueryData(group$.key, result, syncTags))
205
+ }),
206
+ // Catch inside the per-event stream: erroring the group pipe would
207
+ // complete the group's subscription, and since `groupBy` above never
208
+ // removes its group subjects, re-adding the key would emit into a
209
+ // subject with no subscribers — the key could never be fetched again.
210
+ catchError((error) => {
211
+ state.set('setQueryError', setQueryError(group$.key, error))
212
+ return EMPTY
213
+ }),
216
214
  )
217
215
  }),
218
- catchError((error) => {
219
- state.set('setQueryError', setQueryError(group$.key, error))
220
- return EMPTY
221
- }),
222
- tap(({result, syncTags}) => {
223
- state.set('setQueryData', setQueryData(group$.key, result, syncTags))
224
- }),
225
216
  ),
226
217
  ),
227
218
  )
@@ -233,27 +224,14 @@ const listenToLiveClientAndSetLastLiveEventIds = ({
233
224
  instance,
234
225
  key: {resource},
235
226
  }: StoreContext<QueryStoreState, BoundResourceKey>) => {
236
- const liveMessages$ = getClientState(instance, {
237
- apiVersion: QUERY_STORE_API_VERSION,
227
+ const liveMessages$ = observeLiveEvents(instance, {
238
228
  resource,
239
- }).observable.pipe(
240
- switchMap((client) =>
241
- defer(() =>
242
- client.live.events({includeDrafts: !!client.config().token, tag: 'query-store'}),
243
- ).pipe(
244
- catchError((error) => {
245
- if (error instanceof CorsOriginError) {
246
- // Swallow only CORS errors in store without bubbling up so that they are handled by the Cors Error component
247
- state.set('setError', {error})
248
- return EMPTY
249
- }
250
- throw error
251
- }),
252
- ),
253
- ),
254
- share(),
255
- filter((e) => e.type === 'message'),
256
- )
229
+ // Surface CORS errors as store state (handled by the Cors Error
230
+ // component) instead of erroring the stream: a stream error here would
231
+ // reach the outer subscription's errorHandler, set the store-wide
232
+ // `state.error`, and permanently brick every query selector.
233
+ onCorsError: (error) => state.set('setError', {error}),
234
+ }).pipe(share())
257
235
 
258
236
  return state.observable
259
237
  .pipe(
@@ -293,6 +271,16 @@ const listenToLiveClientAndSetLastLiveEventIds = ({
293
271
  *
294
272
  * @beta
295
273
  */
274
+ export function getQueryState<
275
+ TQuery extends string = string,
276
+ TDataset extends string = string,
277
+ TProjectId extends string = string,
278
+ >(
279
+ instance: SanityInstance,
280
+ queryOptions: QueryOptions<TQuery, TDataset, TProjectId>,
281
+ ): StateSource<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`> | undefined>
282
+
283
+ /** @beta */
296
284
  export function getQueryState<TData>(
297
285
  instance: SanityInstance,
298
286
  queryOptions: QueryOptions,
@@ -315,20 +303,20 @@ const _getQueryState = bindActionByResource(
315
303
  createStateSourceAction({
316
304
  selector: ({state, instance}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
317
305
  if (state.error) throw state.error
318
- const key = getQueryKey(normalizeOptionsWithPerspective(instance, options))
306
+ const key = getQueryKey(instance, options)
319
307
  const queryState = state.queries[key]
320
308
  if (queryState?.error) throw queryState.error
321
309
  return queryState?.result
322
310
  },
323
311
  onSubscribe: ({state, instance}, options: QueryOptions) => {
324
312
  const subscriptionId = insecureRandomId()
325
- const key = getQueryKey(normalizeOptionsWithPerspective(instance, options))
313
+ const key = getQueryKey(instance, options)
326
314
 
327
315
  state.set('addSubscriber', addSubscriber(key, subscriptionId))
328
316
 
329
317
  return () => {
330
318
  // this runs on unsubscribe
331
- setTimeout(
319
+ setCleanupTimeout(
332
320
  () => state.set('removeSubscriber', removeSubscriber(key, subscriptionId)),
333
321
  QUERY_STATE_CLEAR_DELAY,
334
322
  )
@@ -341,16 +329,27 @@ const _getQueryState = bindActionByResource(
341
329
  * Resolves the result of a query without registering a lasting subscriber.
342
330
  *
343
331
  * This function fetches the result of a GROQ query and returns a promise that resolves with the query result.
344
- * Unlike `getQueryState`, which registers subscribers to keep the query live and performs automatic cleanup,
345
- * `resolveQuery` does not track subscribers. This makes it ideal for use with React Suspense, where the returned
346
- * promise is thrown to delay rendering until the query result becomes available.
332
+ * It registers a temporary subscriber for the lifetime of the resolution (released shortly after the promise
333
+ * settles), so the query state participates in normal cleanup even when no lasting subscriber ever attaches —
334
+ * e.g. when a suspended component errors before committing. This makes it ideal for use with React Suspense,
335
+ * where the returned promise is thrown to delay rendering until the query result becomes available.
347
336
  * Once the promise resolves, it is expected that a real subscriber will be added via `getQueryState` to manage ongoing updates.
348
337
  *
349
338
  * Additionally, an optional AbortSignal can be provided to cancel the query and immediately clear the associated state
350
- * if there are no active subscribers.
339
+ * if there are no other active subscribers.
351
340
  *
352
341
  * @beta
353
342
  */
343
+ export function resolveQuery<
344
+ TQuery extends string = string,
345
+ TDataset extends string = string,
346
+ TProjectId extends string = string,
347
+ >(
348
+ instance: SanityInstance,
349
+ queryOptions: ResolveQueryOptions<TQuery, TDataset, TProjectId>,
350
+ ): Promise<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>>
351
+
352
+ /** @beta */
354
353
  export function resolveQuery<TData>(
355
354
  instance: SanityInstance,
356
355
  queryOptions: ResolveQueryOptions,
@@ -364,7 +363,15 @@ const _resolveQuery = bindActionByResource(
364
363
  ({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
365
364
  const normalized = normalizeOptionsWithPerspective(instance, options)
366
365
  const {getCurrent} = getQueryState(instance, normalized)
367
- const key = getQueryKey(normalized)
366
+ const key = getQueryKey(instance, normalized)
367
+
368
+ // Hold a temporary subscription for the lifetime of this resolution so the
369
+ // key participates in normal cleanup. Without one, a fetch that errors
370
+ // while a component is suspended leaves a subscriber-less key behind — the
371
+ // component never commits, so no other subscriber exists — and its stored
372
+ // error would be rethrown on every future mount without ever refetching.
373
+ const subscriptionId = insecureRandomId()
374
+ state.set('addSubscriber', addSubscriber(key, subscriptionId))
368
375
 
369
376
  const aborted$ = signal
370
377
  ? new Observable<void>((observer) => {
@@ -383,20 +390,29 @@ const _resolveQuery = bindActionByResource(
383
390
  }).pipe(
384
391
  catchError((error) => {
385
392
  if (error instanceof Error && error.name === 'AbortError') {
386
- state.set('cancelQuery', cancelQuery(key))
393
+ // Release immediately (not after the clear delay) so that when
394
+ // this was the only subscriber, the key removal tears down the
395
+ // in-flight request right away — e.g. when useQuery switches
396
+ // to a different query
397
+ state.set('removeSubscriber', removeSubscriber(key, subscriptionId))
387
398
  }
388
399
  throw error
389
400
  }),
390
401
  )
391
402
  : NEVER
392
403
 
393
- state.set('initializeQuery', initializeQuery(key))
394
-
395
404
  const resolved$ = state.observable.pipe(
396
405
  map(getCurrent),
397
406
  first((i) => i !== undefined),
398
407
  )
399
408
 
400
- return firstValueFrom(race([resolved$, aborted$]))
409
+ const promise = firstValueFrom(race([resolved$, aborted$]))
410
+ const releaseSubscriber = () => {
411
+ setCleanupTimeout(() => {
412
+ state.set('removeSubscriber', removeSubscriber(key, subscriptionId))
413
+ }, QUERY_STATE_CLEAR_DELAY)
414
+ }
415
+ promise.then(releaseSubscriber, releaseSubscriber)
416
+ return promise
401
417
  },
402
418
  )
@@ -2,8 +2,6 @@ import {describe, expect, it} from 'vitest'
2
2
 
3
3
  import {
4
4
  addSubscriber,
5
- cancelQuery,
6
- initializeQuery,
7
5
  type QueryStoreState,
8
6
  removeSubscriber,
9
7
  setLastLiveEventId,
@@ -109,44 +107,4 @@ describe('Query Reducers', () => {
109
107
  expect(newState.queries).not.toHaveProperty('q8')
110
108
  })
111
109
  })
112
-
113
- describe('cancelQuery', () => {
114
- it('should return state unchanged if key does not exist', () => {
115
- const state: QueryStoreState = {queries: {}}
116
- const reducer = cancelQuery('nonexistent')
117
- const newState = reducer(state)
118
- expect(newState).toBe(state)
119
- })
120
-
121
- it('should return state unchanged if query has subscribers', () => {
122
- const state: QueryStoreState = {queries: {q9: {subscribers: ['sub1']}}}
123
- const reducer = cancelQuery('q9')
124
- const newState = reducer(state)
125
- expect(newState).toBe(state)
126
- })
127
-
128
- it('should remove the query if no subscribers exist', () => {
129
- const state: QueryStoreState = {queries: {q10: {subscribers: []}}}
130
- const reducer = cancelQuery('q10')
131
- const newState = reducer(state)
132
- expect(newState.queries).not.toHaveProperty('q10')
133
- })
134
- })
135
-
136
- describe('initializeQuery', () => {
137
- it('should return state unchanged if query already exists', () => {
138
- const existing = {subscribers: ['sub1']}
139
- const state: QueryStoreState = {queries: {q11: existing}}
140
- const reducer = initializeQuery('q11')
141
- const newState = reducer(state)
142
- expect(newState).toBe(state)
143
- })
144
-
145
- it('should add the query with empty subscribers if it does not exist', () => {
146
- const state: QueryStoreState = {queries: {}}
147
- const reducer = initializeQuery('q12')
148
- const newState = reducer(state)
149
- expect(newState.queries['q12']).toEqual({subscribers: []})
150
- })
151
- })
152
110
  })
@@ -1,4 +1,4 @@
1
- import {omit} from 'lodash-es'
1
+ import {omitProperty} from '../utils/object'
2
2
 
3
3
  interface QueryState {
4
4
  syncTags?: string[]
@@ -54,22 +54,6 @@ export const removeSubscriber =
54
54
  const prevQuery = prev.queries[key]
55
55
  if (!prevQuery) return prev
56
56
  const subscribers = prevQuery.subscribers.filter((id) => id !== subscriptionId)
57
- if (!subscribers.length) return {...prev, queries: omit(prev.queries, key)}
57
+ if (!subscribers.length) return {...prev, queries: omitProperty(prev.queries, key)}
58
58
  return {...prev, queries: {...prev.queries, [key]: {...prevQuery, subscribers}}}
59
59
  }
60
-
61
- export const cancelQuery =
62
- (key: string) =>
63
- (prev: QueryStoreState): QueryStoreState => {
64
- const prevQuery = prev.queries[key]
65
- if (!prevQuery) return prev
66
- if (prevQuery.subscribers.length) return prev
67
- return {...prev, queries: omit(prev.queries, key)}
68
- }
69
-
70
- export const initializeQuery =
71
- (key: string) =>
72
- (prev: QueryStoreState): QueryStoreState => {
73
- if (prev.queries[key]) return prev
74
- return {...prev, queries: {...prev.queries, [key]: {subscribers: []}}}
75
- }
@@ -1,14 +1,13 @@
1
+ import {type ReleaseDocument} from '@sanity/client'
1
2
  import {filter, firstValueFrom, of, Subject, take} from 'rxjs'
2
3
  import {describe, expect, it, vi} from 'vitest'
3
4
 
4
- import {type ReleasePerspective, type ResourceHandle} from '../config/sanityConfig'
5
- import {getQueryState} from '../query/queryStore'
5
+ import {type PerspectiveHandle, type ReleasePerspective} from '../config/sanityConfig'
6
6
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
- import {type StateSource} from '../store/createStateSourceAction'
8
7
  import {getPerspectiveState} from './getPerspectiveState'
9
- import {type ReleaseDocument} from './releasesStore'
8
+ import {observeReleases} from './observeReleases'
10
9
 
11
- vi.mock('../query/queryStore')
10
+ vi.mock('./observeReleases')
12
11
 
13
12
  vi.mock('../client/clientStore', () => ({
14
13
  getClientState: vi.fn(() => ({
@@ -39,17 +38,12 @@ describe('getPerspectiveState', () => {
39
38
 
40
39
  // the release store is reversed in getActiveReleases to match UI elsewhere
41
40
  const activeReleases = [release2, release1]
42
- const resource: ResourceHandle['resource'] = {projectId: 'p', dataset: 'd'}
43
41
 
44
42
  beforeEach(() => {
45
- instance = createSanityInstance()
43
+ instance = createSanityInstance({projectId: 'test', dataset: 'test'})
46
44
 
47
45
  mockReleasesQuerySubject = new Subject<ReleaseDocument[] | undefined>()
48
- vi.mocked(getQueryState).mockReturnValue({
49
- subscribe: () => () => {},
50
- getCurrent: () => undefined,
51
- observable: mockReleasesQuerySubject.asObservable(),
52
- } as StateSource<ReleaseDocument[] | undefined>)
46
+ vi.mocked(observeReleases).mockReturnValue(mockReleasesQuerySubject.asObservable())
53
47
  })
54
48
 
55
49
  afterEach(() => {
@@ -58,7 +52,7 @@ describe('getPerspectiveState', () => {
58
52
  })
59
53
 
60
54
  it('should return default perspective if no options or instance perspective is provided', async () => {
61
- const state = getPerspectiveState(instance, {resource})
55
+ const state = getPerspectiveState(instance)
62
56
  mockReleasesQuerySubject.next([])
63
57
  const perspective = await firstValueFrom(state.observable)
64
58
  expect(perspective).toBe('drafts')
@@ -66,14 +60,14 @@ describe('getPerspectiveState', () => {
66
60
 
67
61
  it('should return instance perspective if provided and no options perspective', async () => {
68
62
  instance.config.perspective = 'published'
69
- const state = getPerspectiveState(instance, {resource})
63
+ const state = getPerspectiveState(instance)
70
64
  mockReleasesQuerySubject.next([])
71
65
  const perspective = await firstValueFrom(state.observable)
72
66
  expect(perspective).toBe('published')
73
67
  })
74
68
 
75
69
  it('should return options perspective if provided', async () => {
76
- const options = {perspective: 'raw' as const, resource}
70
+ const options: PerspectiveHandle = {perspective: 'raw'}
77
71
  const state = getPerspectiveState(instance, options)
78
72
  mockReleasesQuerySubject.next([])
79
73
  const perspective = await firstValueFrom(state.observable)
@@ -81,7 +75,7 @@ describe('getPerspectiveState', () => {
81
75
  })
82
76
 
83
77
  it('should return undefined if release perspective is requested but no active releases', async () => {
84
- const options = {perspective: {releaseName: 'release1'}, resource}
78
+ const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
85
79
  const state = getPerspectiveState(instance, options)
86
80
  mockReleasesQuerySubject.next([])
87
81
  const perspective = await firstValueFrom(state.observable)
@@ -89,7 +83,7 @@ describe('getPerspectiveState', () => {
89
83
  })
90
84
 
91
85
  it('should calculate perspective based on active releases and releaseName', async () => {
92
- const options = {perspective: {releaseName: 'release1'}, resource}
86
+ const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
93
87
  const state = getPerspectiveState(instance, options)
94
88
  mockReleasesQuerySubject.next(activeReleases)
95
89
 
@@ -103,7 +97,7 @@ describe('getPerspectiveState', () => {
103
97
  })
104
98
 
105
99
  it('should calculate perspective including multiple releases up to the specified releaseName', async () => {
106
- const options = {perspective: {releaseName: 'release2'}, resource}
100
+ const options: PerspectiveHandle = {perspective: {releaseName: 'release2'}}
107
101
  const state = getPerspectiveState(instance, options)
108
102
  mockReleasesQuerySubject.next(activeReleases)
109
103
  const perspective = await firstValueFrom(
@@ -120,7 +114,7 @@ describe('getPerspectiveState', () => {
120
114
  releaseName: 'release2',
121
115
  excludedPerspectives: ['release1', 'drafts'],
122
116
  }
123
- const options = {perspective: perspectiveConfig, resource}
117
+ const options: PerspectiveHandle = {perspective: perspectiveConfig}
124
118
  const state = getPerspectiveState(instance, options)
125
119
  mockReleasesQuerySubject.next(activeReleases)
126
120
  const perspective = await firstValueFrom(
@@ -133,7 +127,7 @@ describe('getPerspectiveState', () => {
133
127
  })
134
128
 
135
129
  it('should throw if the specified releaseName is not found in active releases', async () => {
136
- const options = {perspective: {releaseName: 'nonexistent'}, resource}
130
+ const options: PerspectiveHandle = {perspective: {releaseName: 'nonexistent'}}
137
131
  const state = getPerspectiveState(instance, options)
138
132
  mockReleasesQuerySubject.next(activeReleases)
139
133
 
@@ -148,8 +142,8 @@ describe('getPerspectiveState', () => {
148
142
  })
149
143
 
150
144
  it('should reuse the same options object for identical inputs (cache test)', async () => {
151
- const options1 = {perspective: {releaseName: 'release1'}, resource}
152
- const options2 = {perspective: {releaseName: 'release1'}, resource}
145
+ const options1: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
146
+ const options2: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
153
147
 
154
148
  const state1 = getPerspectiveState(instance, options1)
155
149
  mockReleasesQuerySubject.next(activeReleases)
@@ -167,7 +161,7 @@ describe('getPerspectiveState', () => {
167
161
  })
168
162
 
169
163
  it('should handle changes in activeReleases (cache test)', async () => {
170
- const options = {perspective: {releaseName: 'release1'}, resource}
164
+ const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
171
165
 
172
166
  const state1 = getPerspectiveState(instance, options)
173
167
  mockReleasesQuerySubject.next(activeReleases)
@@ -3,14 +3,6 @@ import {createSelector} from 'reselect'
3
3
  import {type DocumentResource, type PerspectiveHandle} from '../config/sanityConfig'
4
4
  import {bindActionByResource, type BoundStoreAction} from '../store/createActionBinder'
5
5
  import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
6
- /*
7
- * Although this is an import dependency cycle, it is not a logical cycle:
8
- * 1. getPerspectiveState uses releasesStore as a data source
9
- * 2. releasesStore uses queryStore as a data source
10
- * 3. queryStore calls getPerspectiveState for computing release perspectives
11
- * 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
12
- */
13
- // eslint-disable-next-line import/no-cycle
14
6
  import {releasesStore, type ReleasesStoreState} from './releasesStore'
15
7
  import {isReleasePerspective} from './utils/isReleasePerspective'
16
8
  import {sortReleases} from './utils/sortReleases'
@@ -26,7 +18,7 @@ const selectActiveReleases = (context: SelectorContext<ReleasesStoreState>) =>
26
18
  context.state.activeReleases
27
19
  const selectOptions = (
28
20
  _context: SelectorContext<ReleasesStoreState>,
29
- options: PerspectiveHandle & {resource?: DocumentResource},
21
+ options: PerspectiveHandle & {projectId?: string; dataset?: string; resource?: DocumentResource},
30
22
  ) => options
31
23
 
32
24
  const memoizedOptionsSelector = createSelector(
@@ -53,7 +45,6 @@ const memoizedOptionsSelector = createSelector(
53
45
  },
54
46
  )
55
47
 
56
- // Lazily bind the action itself to avoid circular import initialization issues with `releasesStore`
57
48
  const _getPerspectiveStateSelector = createStateSourceAction({
58
49
  selector: createSelector(
59
50
  [selectInstancePerspective, selectActiveReleases, memoizedOptionsSelector],
@@ -89,7 +80,10 @@ type BoundGetPerspectiveState = BoundStoreAction<
89
80
  ReturnType<typeof _getPerspectiveStateSelector>
90
81
  >
91
82
 
92
- let _boundGetPerspectiveState: BoundGetPerspectiveState | undefined
83
+ const _boundGetPerspectiveState = bindActionByResource(
84
+ releasesStore,
85
+ _getPerspectiveStateSelector,
86
+ ) as BoundGetPerspectiveState
93
87
 
94
88
  /**
95
89
  * Provides a subscribable state source for a "perspective" for the Sanity client,
@@ -103,12 +97,6 @@ let _boundGetPerspectiveState: BoundGetPerspectiveState | undefined
103
97
  *
104
98
  * @public
105
99
  */
106
- export const getPerspectiveState: BoundGetPerspectiveState = (instance, ...rest) => {
107
- if (!_boundGetPerspectiveState) {
108
- _boundGetPerspectiveState = bindActionByResource(
109
- releasesStore,
110
- _getPerspectiveStateSelector,
111
- ) as BoundGetPerspectiveState
112
- }
113
- return _boundGetPerspectiveState(instance, ...rest)
114
- }
100
+ export const getPerspectiveState: BoundGetPerspectiveState = (instance, ...rest) =>
101
+ // bindActionByResource keyFn destructures { resource } from the first param, so pass {} when no options
102
+ _boundGetPerspectiveState(instance, ...(rest.length ? rest : [{}]))