@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,7 +1,7 @@
1
- import {type Action} from '@sanity/client'
2
- import {getPublishedId} from '@sanity/client/csm'
1
+ import {type Action, ClientError, CorsOriginError, type Mutation} from '@sanity/client'
2
+ import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
3
3
  import {jsonMatch} from '@sanity/json-match'
4
- import {type SanityDocument} from '@sanity/types'
4
+ import {type SanityDocument} from 'groq'
5
5
  import {type ExprNode} from 'groq-js'
6
6
  import {
7
7
  catchError,
@@ -17,15 +17,18 @@ import {
17
17
  Observable,
18
18
  of,
19
19
  pairwise,
20
+ retry,
20
21
  startWith,
21
22
  Subject,
22
23
  switchMap,
23
24
  tap,
24
25
  throttle,
26
+ throwError,
25
27
  timer,
26
28
  withLatestFrom,
27
29
  } from 'rxjs'
28
30
 
31
+ import {getCurrentUserState} from '../auth/authStore'
29
32
  import {type ClientOptions, getClientState} from '../client/clientStore'
30
33
  import {
31
34
  type DocumentHandle,
@@ -34,6 +37,7 @@ import {
34
37
  isDatasetResource,
35
38
  isMediaLibraryResource,
36
39
  } from '../config/sanityConfig'
40
+ import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
37
41
  import {
38
42
  bindActionByResource,
39
43
  type BoundResourceKey,
@@ -42,11 +46,22 @@ import {
42
46
  import {type SanityInstance} from '../store/createSanityInstance'
43
47
  import {createStateSourceAction, type StateSource} from '../store/createStateSourceAction'
44
48
  import {defineStore, type StoreContext} from '../store/defineStore'
45
- import {getDraftId} from '../utils/ids'
46
49
  import {type DocumentAction} from './actions'
47
- import {API_VERSION, INITIAL_OUTGOING_THROTTLE_TIME} from './documentConstants'
48
- import {type DocumentEvent, getDocumentEvents} from './events'
50
+ import {
51
+ ACL_RETRY_BASE_DELAY,
52
+ ACL_RETRY_MAX_DELAY,
53
+ API_VERSION,
54
+ INITIAL_OUTGOING_THROTTLE_TIME,
55
+ OUT_OF_SYNC_RETRY_BASE_DELAY,
56
+ OUT_OF_SYNC_RETRY_MAX_DELAY,
57
+ } from './documentConstants'
58
+ import {
59
+ type DocumentEvent,
60
+ type DocumentTransactionSubmissionResult,
61
+ getDocumentEvents,
62
+ } from './events'
49
63
  import {listen, OutOfSyncError} from './listen'
64
+ import {type JsonMatch} from './patchOperations'
50
65
  import {
51
66
  calculatePermissions,
52
67
  createGrantsLookup,
@@ -54,13 +69,13 @@ import {
54
69
  type DocumentPermissionsResult,
55
70
  type Grant,
56
71
  } from './permissions'
57
- import {ActionError} from './processActions'
72
+ import {ActionError} from './processActions/processActions'
73
+ import {isReleaseAction} from './processActions/releaseUtil'
58
74
  import {
59
75
  type AppliedTransaction,
60
76
  applyFirstQueuedTransaction,
61
77
  applyRemoteDocument,
62
78
  cleanupOutgoingTransaction,
63
- getDocumentIdsFromActions,
64
79
  manageSubscriberIds,
65
80
  type OutgoingTransaction,
66
81
  type QueuedTransaction,
@@ -77,6 +92,10 @@ export interface DocumentStoreState {
77
92
  applied: AppliedTransaction[]
78
93
  outgoing?: OutgoingTransaction
79
94
  grants?: Record<Grant, ExprNode>
95
+ /**
96
+ * The current user's identity (their user ID).
97
+ */
98
+ identity?: string
80
99
  error?: unknown
81
100
  sharedListener: SharedListener
82
101
  fetchDocument: (documentId: string) => Observable<SanityDocument | null>
@@ -116,6 +135,14 @@ export interface DocumentState {
116
135
  * local transactions on top of this new remote.
117
136
  */
118
137
  unverifiedRevisions?: {[TTransactionId in string]?: UnverifiedDocumentRevision}
138
+ /**
139
+ * transaction IDs recently submitted by this client for this document,
140
+ * newest last. used to label `remote-patches` events with the correct
141
+ * `origin` even after the corresponding `unverifiedRevisions` entry has
142
+ * been pruned (e.g. by a sync event that raced the listener echo). capped
143
+ * to the most recent entries.
144
+ */
145
+ recentOwnTransactionIds?: string[]
119
146
  }
120
147
 
121
148
  export const documentStore = defineStore<DocumentStoreState, BoundResourceKey>({
@@ -136,6 +163,7 @@ export const documentStore = defineStore<DocumentStoreState, BoundResourceKey>({
136
163
  subscribeToSubscriptionsAndListenToDocuments(context),
137
164
  subscribeToAppliedAndSubmitNextTransaction(context),
138
165
  subscribeToClientAndFetchDatasetAcl(context),
166
+ subscribeToCurrentUserAndSetIdentity(context),
139
167
  ]
140
168
 
141
169
  return () => {
@@ -158,6 +186,29 @@ export interface DocumentOptions<
158
186
  path?: TPath
159
187
  }
160
188
 
189
+ /** @beta */
190
+ export function getDocumentState<
191
+ TDocumentType extends string = string,
192
+ TDataset extends string = string,
193
+ TProjectId extends string = string,
194
+ >(
195
+ instance: SanityInstance,
196
+ options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
197
+ ): StateSource<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | undefined | null>
198
+
199
+ /** @beta */
200
+ export function getDocumentState<
201
+ TPath extends string = string,
202
+ TDocumentType extends string = string,
203
+ TDataset extends string = string,
204
+ TProjectId extends string = string,
205
+ >(
206
+ instance: SanityInstance,
207
+ options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
208
+ ): StateSource<
209
+ JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath> | undefined
210
+ >
211
+
161
212
  /** @beta */
162
213
  export function getDocumentState<TData>(
163
214
  instance: SanityInstance,
@@ -175,29 +226,28 @@ const _getDocumentState = bindActionByResource(
175
226
  documentStore,
176
227
  createStateSourceAction({
177
228
  selector: ({state: {error, documentStates}}, options: DocumentOptions<string | undefined>) => {
178
- const {documentId, path, liveEdit} = options
229
+ const {documentId: docId, path, liveEdit, perspective} = options
230
+ const documentId = DocumentId(docId)
179
231
  if (error) throw error
232
+ let document: SanityDocument | null | undefined
180
233
 
181
234
  if (liveEdit) {
182
- // For liveEdit documents, only look at the single document
183
- const document = documentStates[documentId]?.local
184
- if (document === undefined) return undefined
185
- if (!path) return document
186
- const result = jsonMatch(document, path).next()
187
- if (result.done) return undefined
188
- const {value} = result.value
189
- return value
235
+ document = documentStates[documentId]?.local
236
+ } else {
237
+ let version: SanityDocument | null | undefined
238
+ if (isReleasePerspective(perspective)) {
239
+ const versionId = getVersionId(documentId, perspective.releaseName)
240
+ version = documentStates[versionId]?.local
241
+ // early exit if we don't have the version document and we're in a release perspective
242
+ if (version === undefined) return undefined
243
+ }
244
+ const draft = documentStates[getDraftId(documentId)]?.local
245
+ const published = documentStates[getPublishedId(documentId)]?.local
246
+ // early exit if we don't have all the documents for draft/published logic
247
+ if (draft === undefined || published === undefined) return undefined
248
+ document = version ?? draft ?? published
190
249
  }
191
250
 
192
- // Standard draft/published logic
193
- const draftId = getDraftId(documentId)
194
- const publishedId = getPublishedId(documentId)
195
- const draft = documentStates[draftId]?.local
196
- const published = documentStates[publishedId]?.local
197
-
198
- // wait for draft and published to be loaded before returning a value
199
- if (draft === undefined || published === undefined) return undefined
200
- const document = draft ?? published
201
251
  if (!path) return document
202
252
  const result = jsonMatch(document, path).next()
203
253
  if (result.done) return undefined
@@ -205,20 +255,24 @@ const _getDocumentState = bindActionByResource(
205
255
  return value
206
256
  },
207
257
  onSubscribe: (context, options: DocumentOptions<string | undefined>) =>
208
- manageSubscriberIds(context, options.documentId, {expandDraftPublished: !options.liveEdit}),
258
+ manageSubscriberIds(context, [options]),
209
259
  }),
210
260
  )
211
261
 
212
262
  /** @beta */
213
- export function resolveDocument<TData extends SanityDocument>(
263
+ export function resolveDocument<
264
+ TDocumentType extends string = string,
265
+ TDataset extends string = string,
266
+ TProjectId extends string = string,
267
+ >(
214
268
  instance: SanityInstance,
215
- docHandle: DocumentHandle,
216
- ): Promise<TData | null>
269
+ docHandle: DocumentHandle<TDocumentType, TDataset, TProjectId>,
270
+ ): Promise<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null>
217
271
  /** @beta */
218
- export function resolveDocument(
272
+ export function resolveDocument<TData extends SanityDocument>(
219
273
  instance: SanityInstance,
220
- docHandle: DocumentHandle,
221
- ): Promise<SanityDocument | null>
274
+ docHandle: DocumentHandle<string, string, string>,
275
+ ): Promise<TData | null>
222
276
  /** @beta */
223
277
  export function resolveDocument(
224
278
  ...args: Parameters<typeof _resolveDocument>
@@ -245,32 +299,32 @@ export const getDocumentSyncStatus = bindActionByResource(
245
299
  {state: {error, documentStates: documents, outgoing, applied, queued}},
246
300
  doc: DocumentHandle,
247
301
  ) => {
248
- const documentId = typeof doc === 'string' ? doc : doc.documentId
302
+ const documentId = DocumentId(typeof doc === 'string' ? doc : doc.documentId)
249
303
  if (error) throw error
250
304
 
251
305
  if (doc.liveEdit) {
252
306
  // For liveEdit documents, only check the single document
253
- const document = documents[documentId]
254
- if (document === undefined) return undefined
255
- return !queued.length && !applied.length && !outgoing
307
+ if (documents[documentId] === undefined) return undefined
308
+ } else {
309
+ const version = isReleasePerspective(doc.perspective)
310
+ ? documents[getVersionId(documentId, doc.perspective.releaseName)]
311
+ : undefined
312
+ if (isReleasePerspective(doc.perspective) && version === undefined) return undefined
313
+ // Standard draft/published logic
314
+ const draft = documents[getDraftId(documentId)]
315
+ const published = documents[getPublishedId(documentId)]
316
+ if (draft === undefined || published === undefined) return undefined
256
317
  }
257
-
258
- // Standard draft/published logic
259
- const draftId = getDraftId(documentId)
260
- const publishedId = getPublishedId(documentId)
261
-
262
- const draft = documents[draftId]
263
- const published = documents[publishedId]
264
-
265
- if (draft === undefined || published === undefined) return undefined
266
318
  return !queued.length && !applied.length && !outgoing
267
319
  },
268
- onSubscribe: (context, doc: DocumentHandle) => manageSubscriberIds(context, doc.documentId),
320
+ onSubscribe: (context, doc: DocumentHandle) => {
321
+ return manageSubscriberIds(context, [doc])
322
+ },
269
323
  }),
270
324
  )
271
325
 
272
326
  type PermissionsStateOptions = {
273
- resource: DocumentResource
327
+ resource?: DocumentResource
274
328
  actions: DocumentAction[]
275
329
  }
276
330
 
@@ -279,8 +333,9 @@ export const getPermissionsState = bindActionByResource(
279
333
  documentStore,
280
334
  createStateSourceAction({
281
335
  selector: calculatePermissions,
282
- onSubscribe: (context, {actions}: PermissionsStateOptions) =>
283
- manageSubscriberIds(context, getDocumentIdsFromActions(actions)),
336
+ onSubscribe: (context, {actions}: PermissionsStateOptions) => {
337
+ manageSubscriberIds(context, actions)
338
+ },
284
339
  }) as StoreAction<
285
340
  DocumentStoreState,
286
341
  [PermissionsStateOptions],
@@ -301,7 +356,7 @@ export const resolvePermissions = bindActionByResource(
301
356
  /** @beta */
302
357
  export const subscribeDocumentEvents = bindActionByResource(
303
358
  documentStore,
304
- ({state}, options: {resource: DocumentResource; eventHandler: (e: DocumentEvent) => void}) => {
359
+ ({state}, options: {resource?: DocumentResource; eventHandler: (e: DocumentEvent) => void}) => {
305
360
  const {events} = state.get()
306
361
  const subscription = events.subscribe(options.eventHandler)
307
362
  return () => subscription.unsubscribe()
@@ -365,24 +420,48 @@ const subscribeToAppliedAndSubmitNextTransaction = ({
365
420
  withLatestFrom(
366
421
  getClientState(instance, {
367
422
  apiVersion: API_VERSION,
368
- ...(resource ? {resource} : {}),
423
+ resource,
369
424
  }).observable,
370
425
  ),
371
426
  concatMap(([outgoing, client]) => {
372
427
  if (!outgoing) return EMPTY
428
+
429
+ const revertOnError = catchError((error: unknown) => {
430
+ state.set('revertOutgoingTransaction', revertOutgoingTransaction)
431
+ const message = error instanceof Error ? error.message : 'Request failed'
432
+ events.next({type: 'reverted', message, outgoing, error})
433
+ return EMPTY
434
+ })
435
+
436
+ const toResult = map((result: unknown) => ({
437
+ result: result as DocumentTransactionSubmissionResult,
438
+ outgoing,
439
+ }))
440
+
441
+ // liveEdit transactions route to the mutations API; everything else routes
442
+ // to the actions API. processActions rejects transactions that mix the two,
443
+ // and reducers won't batch across that boundary, so a batch is always
444
+ // entirely liveEdit or entirely not.
445
+ if (outgoing.actions.some((action) => !isReleaseAction(action) && action.liveEdit)) {
446
+ return client.observable
447
+ .mutate(outgoing.outgoingMutations as Mutation[], {
448
+ transactionId: outgoing.transactionId,
449
+ visibility: 'async',
450
+ returnDocuments: false,
451
+ returnFirst: false,
452
+ tag: 'document.mutate',
453
+ skipCrossDatasetReferenceValidation: true,
454
+ })
455
+ .pipe(revertOnError, toResult)
456
+ }
457
+
373
458
  return client.observable
374
459
  .action(outgoing.outgoingActions as Action[], {
375
460
  transactionId: outgoing.transactionId,
376
461
  skipCrossDatasetReferenceValidation: true,
462
+ tag: 'document.action',
377
463
  })
378
- .pipe(
379
- catchError((error) => {
380
- state.set('revertOutgoingTransaction', revertOutgoingTransaction)
381
- events.next({type: 'reverted', message: error.message, outgoing, error})
382
- return EMPTY
383
- }),
384
- map((result) => ({result, outgoing})),
385
- )
464
+ .pipe(revertOnError, toResult)
386
465
  }),
387
466
  tap(({outgoing, result}) => {
388
467
  state.set('cleanupOutgoingTransaction', cleanupOutgoingTransaction)
@@ -424,8 +503,8 @@ const subscribeToSubscriptionsAndListenToDocuments = (
424
503
  ...added.map((id) => ({id, add: true})),
425
504
  ...removed.map((id) => ({id, add: false})),
426
505
  ].sort((a, b) => {
427
- const aIsDraft = a.id === getDraftId(a.id)
428
- const bIsDraft = b.id === getDraftId(b.id)
506
+ const aIsDraft = a.id === getDraftId(DocumentId(a.id))
507
+ const bIsDraft = b.id === getDraftId(DocumentId(b.id))
429
508
 
430
509
  if (aIsDraft && bIsDraft) return a.id.localeCompare(b.id, 'en-US')
431
510
  if (aIsDraft) return -1
@@ -441,10 +520,15 @@ const subscribeToSubscriptionsAndListenToDocuments = (
441
520
  switchMap((e) => {
442
521
  if (!e.add) return EMPTY
443
522
  return listen(context, e.id).pipe(
444
- catchError((error) => {
445
- // retry on `OutOfSyncError`
446
- if (error instanceof OutOfSyncError) listen(context, e.id)
447
- throw error
523
+ retry({
524
+ delay: (error, retryCount) => {
525
+ if (!(error instanceof OutOfSyncError)) return throwError(() => error)
526
+ const backoff = Math.min(
527
+ OUT_OF_SYNC_RETRY_BASE_DELAY * 2 ** (retryCount - 1),
528
+ OUT_OF_SYNC_RETRY_MAX_DELAY,
529
+ )
530
+ return timer(backoff)
531
+ },
448
532
  }),
449
533
  tap((remote) =>
450
534
  state.set('applyRemoteDocument', (prev) =>
@@ -464,10 +548,7 @@ const subscribeToClientAndFetchDatasetAcl = ({
464
548
  state,
465
549
  key: {resource},
466
550
  }: StoreContext<DocumentStoreState, BoundResourceKey>) => {
467
- const clientOptions: ClientOptions = {apiVersion: API_VERSION}
468
- if (resource) {
469
- clientOptions.resource = resource
470
- }
551
+ const clientOptions: ClientOptions = {apiVersion: API_VERSION, resource}
471
552
 
472
553
  let uri: string
473
554
  if (resource && isDatasetResource(resource)) {
@@ -483,11 +564,39 @@ const subscribeToClientAndFetchDatasetAcl = ({
483
564
  return getClientState(instance, clientOptions)
484
565
  .observable.pipe(
485
566
  switchMap((client) =>
486
- client.observable.request<DatasetAcl>({
487
- uri,
488
- tag: 'acl.get',
489
- withCredentials: true,
490
- }),
567
+ client.observable
568
+ .request<DatasetAcl>({
569
+ uri,
570
+ tag: 'acl.get',
571
+ })
572
+ .pipe(
573
+ retry({
574
+ delay: (error, retryCount) => {
575
+ // 4xx responses and CORS misconfigurations are not transient —
576
+ // the server will keep rejecting the request, so rethrow to
577
+ // surface them as a fatal store error. 408 (request timeout)
578
+ // and 429 (rate limit) are the exceptions: they resolve on
579
+ // their own, so they are retried like network errors
580
+ const isTransientClientError =
581
+ error instanceof ClientError &&
582
+ (error.statusCode === 408 || error.statusCode === 429)
583
+ if (
584
+ (error instanceof ClientError && !isTransientClientError) ||
585
+ error instanceof CorsOriginError
586
+ ) {
587
+ return throwError(() => error)
588
+ }
589
+ // network errors (no status code), 408/429 responses, and 5xx
590
+ // responses are retried with exponential backoff so a transient
591
+ // failure during startup doesn't permanently brick the store
592
+ const backoff = Math.min(
593
+ ACL_RETRY_BASE_DELAY * 2 ** (retryCount - 1),
594
+ ACL_RETRY_MAX_DELAY,
595
+ )
596
+ return timer(backoff)
597
+ },
598
+ }),
599
+ ),
491
600
  ),
492
601
  tap((datasetAcl) => state.set('setGrants', {grants: createGrantsLookup(datasetAcl)})),
493
602
  )
@@ -495,3 +604,16 @@ const subscribeToClientAndFetchDatasetAcl = ({
495
604
  error: (error) => state.set('setError', {error}),
496
605
  })
497
606
  }
607
+
608
+ const subscribeToCurrentUserAndSetIdentity = ({
609
+ instance,
610
+ state,
611
+ }: StoreContext<DocumentStoreState, BoundResourceKey>) =>
612
+ getCurrentUserState(instance).observable.subscribe({
613
+ next: (currentUser) => state.set('setIdentity', {identity: currentUser?.id}),
614
+ // A transient identity-fetch failure (network blip, expired token, or a
615
+ // normal logout/re-login transition) should not brick all document
616
+ // operations. Reset the identity to `undefined` and keep going — GROQ's
617
+ // `identity()` then evaluates to null, matching the unauthenticated state.
618
+ error: () => state.set('setIdentity', {identity: undefined}),
619
+ })
@@ -1,6 +1,6 @@
1
1
  import {describe, expect, it} from 'vitest'
2
2
 
3
- import {type DocumentAction} from '../document/actions'
3
+ import {type Action, type DocumentAction} from '../document/actions'
4
4
  import {type DocumentEvent, getDocumentEvents} from '../document/events'
5
5
  import {type OutgoingTransaction} from '../document/reducers'
6
6
 
@@ -41,11 +41,66 @@ describe('getDocumentEvents', () => {
41
41
  expect(events).toHaveLength(outgoing.actions.length)
42
42
  events.forEach((event) => {
43
43
  const action = outgoing.actions.find(
44
- (a) => 'documentId' in event && event.documentId === a.documentId,
44
+ (a) => 'documentId' in a && 'documentId' in event && event.documentId === a.documentId,
45
45
  )
46
46
  expect(action).toBeDefined()
47
47
  expect(event.type).toEqual(expectedMap[action!.type])
48
48
  expect((event as Extract<DocumentEvent, {outgoing: unknown}>).outgoing).toBe(outgoing)
49
49
  })
50
50
  })
51
+
52
+ it('emits created/edited/deleted events for release.create/edit/delete with release doc IDs', () => {
53
+ const outgoing: OutgoingTransaction = {
54
+ transactionId: 'txn-release',
55
+ actions: [
56
+ {type: 'release.create', releaseId: 'r1'} as Action,
57
+ {type: 'release.edit', releaseId: 'r2', patch: {set: {}}} as Action,
58
+ {type: 'release.delete', releaseId: 'r3'} as Action,
59
+ ],
60
+ disableBatching: false,
61
+ batchedTransactionIds: [],
62
+ outgoingActions: [],
63
+ outgoingMutations: [],
64
+ base: {},
65
+ working: {},
66
+ previous: {},
67
+ previousRevs: {},
68
+ timestamp: '2025-02-06T00:00:00.000Z',
69
+ }
70
+
71
+ const events = getDocumentEvents(outgoing)
72
+ expect(events).toEqual([
73
+ {type: 'created', documentId: '_.releases.r1', outgoing},
74
+ {type: 'edited', documentId: '_.releases.r2', outgoing},
75
+ {type: 'deleted', documentId: '_.releases.r3', outgoing},
76
+ ])
77
+ })
78
+
79
+ it('skips release actions that have no local mutation (publish/schedule/archive/etc.)', () => {
80
+ const outgoing: OutgoingTransaction = {
81
+ transactionId: 'txn-release-noop',
82
+ actions: [
83
+ {type: 'release.publish', releaseId: 'r1'} as Action,
84
+ {
85
+ type: 'release.schedule',
86
+ releaseId: 'r2',
87
+ publishAt: '2026-01-01T00:00:00.000Z',
88
+ } as Action,
89
+ {type: 'release.unschedule', releaseId: 'r3'} as Action,
90
+ {type: 'release.archive', releaseId: 'r4'} as Action,
91
+ {type: 'release.unarchive', releaseId: 'r5'} as Action,
92
+ ],
93
+ disableBatching: false,
94
+ batchedTransactionIds: [],
95
+ outgoingActions: [],
96
+ outgoingMutations: [],
97
+ base: {},
98
+ working: {},
99
+ previous: {},
100
+ previousRevs: {},
101
+ timestamp: '2025-02-06T00:00:00.000Z',
102
+ }
103
+
104
+ expect(getDocumentEvents(outgoing)).toEqual([])
105
+ })
51
106
  })
@@ -1,8 +1,15 @@
1
- import {type SanityClient} from '@sanity/client'
1
+ import {type MultipleMutationResult, type SanityClient} from '@sanity/client'
2
+ import {type PatchOperations} from '@sanity/types'
2
3
 
3
- import {type DocumentAction} from './actions'
4
+ import {type DocumentAction, type ReleaseAction} from './actions'
5
+ import {getReleaseDocumentId, isReleaseAction} from './processActions/releaseUtil'
4
6
  import {type OutgoingTransaction} from './reducers'
5
7
 
8
+ /** @beta Response body from submitting an outgoing transaction (actions or mutations API). */
9
+ export type DocumentTransactionSubmissionResult =
10
+ | Awaited<ReturnType<SanityClient['action']>>
11
+ | MultipleMutationResult
12
+
6
13
  /** @beta */
7
14
  export type DocumentEvent =
8
15
  | ActionErrorEvent
@@ -15,6 +22,7 @@ export type DocumentEvent =
15
22
  | DocumentPublishedEvent
16
23
  | DocumentUnpublishedEvent
17
24
  | DocumentDiscardedEvent
25
+ | DocumentRemotePatchesEvent
18
26
 
19
27
  /**
20
28
  * @beta
@@ -35,7 +43,7 @@ export interface ActionErrorEvent {
35
43
  export interface TransactionAcceptedEvent {
36
44
  type: 'accepted'
37
45
  outgoing: OutgoingTransaction
38
- result: Awaited<ReturnType<SanityClient['action']>>
46
+ result: DocumentTransactionSubmissionResult
39
47
  }
40
48
  /**
41
49
  * @beta
@@ -113,31 +121,79 @@ export interface DocumentDiscardedEvent {
113
121
  outgoing: OutgoingTransaction
114
122
  }
115
123
 
116
- export function getDocumentEvents(outgoing: OutgoingTransaction): DocumentEvent[] {
117
- const documentIdsByAction = Object.entries(
118
- outgoing.actions.reduce(
119
- (acc, {type, documentId}) => {
120
- const ids = acc[type] || new Set()
121
- if (documentId) ids.add(documentId)
122
- acc[type] = ids
123
- return acc
124
- },
125
- {} as Record<DocumentAction['type'], Set<string>>,
126
- ),
127
- ) as [DocumentAction['type'], Set<string>][]
124
+ /**
125
+ * @beta
126
+ * Event emitted when patches for a document are observed through the listener.
127
+ *
128
+ * Unlike the whole-document snapshots the store keeps, these are the raw patch
129
+ * operations from the transaction that produced the change, preserving the
130
+ * operational intent of the edit (e.g. keyed array inserts/unsets). Consumers
131
+ * that maintain their own local state, like collaborative text editors, can
132
+ * apply these directly instead of diffing document snapshots.
133
+ *
134
+ * `origin` is `'local'` when the transaction was submitted by this document
135
+ * store instance (an own edit observed back through the listener) and
136
+ * `'remote'` when it came from another client.
137
+ */
138
+ export interface DocumentRemotePatchesEvent {
139
+ type: 'remote-patches'
140
+ /** The ID of the document version the patches apply to (e.g. a draft ID). */
141
+ documentId: string
142
+ /** The transaction ID that carried these patches. */
143
+ transactionId: string
144
+ /** The revision the document was at before this transaction. */
145
+ previousRev?: string
146
+ /** The timestamp of the transaction. */
147
+ timestamp: string
148
+ /** The patch operations, rooted at the document. */
149
+ patches: PatchOperations[]
150
+ /** Whether the transaction originated from this client or another one. */
151
+ origin: 'local' | 'remote'
152
+ }
153
+
154
+ // Release actions that write a mutation to the local release doc map onto
155
+ // the regular per-document events with `documentId = '_.releases.<releaseId>'`.
156
+ // The other release actions (publish/schedule/unschedule/archive/unarchive)
157
+ // don't mutate local state, so they aren't in the map and get skipped — they
158
+ // surface through the transaction-level `accepted`/`reverted` events instead.
159
+ const actionMap = {
160
+ 'document.create': 'created',
161
+ 'document.delete': 'deleted',
162
+ 'document.discard': 'discarded',
163
+ 'document.edit': 'edited',
164
+ 'document.publish': 'published',
165
+ 'document.unpublish': 'unpublished',
166
+ 'release.create': 'created',
167
+ 'release.edit': 'edited',
168
+ 'release.delete': 'deleted',
169
+ } satisfies Partial<Record<DocumentAction['type'] | ReleaseAction['type'], DocumentEvent['type']>>
128
170
 
129
- const actionMap = {
130
- 'document.create': 'created',
131
- 'document.delete': 'deleted',
132
- 'document.discard': 'discarded',
133
- 'document.edit': 'edited',
134
- 'document.publish': 'published',
135
- 'document.unpublish': 'unpublished',
136
- } satisfies Record<DocumentAction['type'], DocumentEvent['type']>
171
+ type MappedActionType = keyof typeof actionMap
172
+
173
+ export function getDocumentEvents(outgoing: OutgoingTransaction): DocumentEvent[] {
174
+ const documentIdsByAction = outgoing.actions.reduce(
175
+ (acc, action) => {
176
+ if (!(action.type in actionMap)) return acc
177
+ const documentId = isReleaseAction(action)
178
+ ? getReleaseDocumentId(action.releaseId)
179
+ : action.documentId
180
+ if (!documentId) return acc
181
+ const type = action.type as MappedActionType
182
+ const ids = acc[type] ?? new Set<string>()
183
+ ids.add(documentId)
184
+ acc[type] = ids
185
+ return acc
186
+ },
187
+ {} as Partial<Record<MappedActionType, Set<string>>>,
188
+ )
137
189
 
138
- return documentIdsByAction.flatMap(([actionType, documentIds]) =>
139
- Array.from(documentIds).map(
140
- (documentId): DocumentEvent => ({type: actionMap[actionType], documentId, outgoing}),
190
+ return Object.entries(documentIdsByAction).flatMap(([actionType, documentIds]) =>
191
+ Array.from(documentIds ?? []).map(
192
+ (documentId): DocumentEvent => ({
193
+ type: actionMap[actionType as MappedActionType],
194
+ documentId,
195
+ outgoing,
196
+ }),
141
197
  ),
142
198
  )
143
199
  }