@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,35 +1,148 @@
1
1
  import {switchMap} from 'rxjs'
2
2
 
3
3
  import {getClientState} from '../client/clientStore'
4
- import {createFetcherStore} from '../utils/createFetcherStore'
4
+ import {type ProjectHandle} from '../config/sanityConfig'
5
+ import {projects} from '../projects/projects'
6
+ import {type SanityInstance} from '../store/createSanityInstance'
7
+ import {defineFetcher} from '../store/fetcherStore'
8
+ import {buildQuery} from '../utils/buildQuery'
5
9
 
6
10
  const API_VERSION = 'v2025-02-19'
7
11
 
8
12
  /** @public */
9
- export type ProjectHandle = {
10
- projectId: string
11
- }
12
- const project = createFetcherStore({
13
- name: 'Project',
14
- getKey: (_instance, options: ProjectHandle) => {
15
- const projectId = options?.projectId
16
- if (!projectId) {
17
- throw new Error('A projectId is required to use the project API.')
18
- }
19
- return projectId
20
- },
21
- fetcher: (instance) => (options: ProjectHandle) => {
22
- const projectId = options.projectId
23
-
24
- return getClientState(instance, {
25
- apiVersion: API_VERSION,
26
- scope: 'global',
27
- projectId,
28
- }).observable.pipe(switchMap((client) => client.observable.projects.getById(projectId!)))
29
- },
30
- })
13
+ export interface ProjectMemberRole {
14
+ name: string
15
+ title: string
16
+ description: string
17
+ }
31
18
 
32
19
  /** @public */
33
- export const getProjectState = project.getState
20
+ export interface ProjectMember {
21
+ id: string
22
+ createdAt: string
23
+ updatedAt: string
24
+ isCurrentUser: boolean
25
+ isRobot: boolean
26
+ roles: ProjectMemberRole[]
27
+ }
28
+
29
+ /** @public */
30
+ export interface ProjectMetadata {
31
+ color?: string
32
+ externalStudioHost?: string
33
+ initialTemplate?: string
34
+ cliInitializedAt?: string
35
+ integration: 'manage' | 'cli'
36
+ }
37
+
38
+ /**
39
+ * The base fields returned from `/projects` for every project.
40
+ * @public
41
+ */
42
+ export interface ProjectBase {
43
+ id: string
44
+ displayName: string
45
+ studioHost: string | null
46
+ organizationId: string
47
+ metadata: ProjectMetadata
48
+ isBlocked: boolean
49
+ isDisabled: boolean
50
+ isDisabledByUser: boolean
51
+ activityFeedEnabled: boolean
52
+ createdAt: string
53
+ updatedAt: string
54
+ }
55
+
56
+ /**
57
+ * A `Project` with `members` and/or `features` conditionally included
58
+ * based on the query options used to fetch it.
59
+ * @public
60
+ */
61
+ export type Project<
62
+ IncludeMembers extends boolean = true,
63
+ IncludeFeatures extends boolean = true,
64
+ > = ProjectBase &
65
+ // `boolean extends T` is non-distributive — true only when T is the wide
66
+ // `boolean`, in which case the field is optional. Literal `true`/`false`
67
+ // fall through to the strict branch.
68
+ (boolean extends IncludeMembers
69
+ ? {members?: ProjectMember[]}
70
+ : IncludeMembers extends true
71
+ ? {members: ProjectMember[]}
72
+ : unknown) &
73
+ (boolean extends IncludeFeatures
74
+ ? {features?: string[]}
75
+ : IncludeFeatures extends true
76
+ ? {features: string[]}
77
+ : unknown)
78
+
34
79
  /** @public */
35
- export const resolveProject = project.resolveState
80
+ export interface ProjectOptions<
81
+ IncludeMembers extends boolean = true,
82
+ IncludeFeatures extends boolean = true,
83
+ > extends ProjectHandle {
84
+ includeMembers?: IncludeMembers
85
+ includeFeatures?: IncludeFeatures
86
+ }
87
+
88
+ function normalizeProjectOptions(options?: ProjectOptions<boolean, boolean>) {
89
+ return {
90
+ includeMembers: options?.includeMembers ?? true,
91
+ includeFeatures: options?.includeFeatures ?? true,
92
+ }
93
+ }
94
+
95
+ function resolveProjectId(instance: SanityInstance, options?: ProjectOptions<boolean, boolean>) {
96
+ const projectId = options?.projectId ?? instance.config.projectId
97
+ if (!projectId) {
98
+ throw new Error('A projectId is required to use the project API.')
99
+ }
100
+ return projectId
101
+ }
102
+
103
+ /** @internal */
104
+ export function getProjectCacheKey(
105
+ instance: SanityInstance,
106
+ options?: ProjectOptions<boolean, boolean>,
107
+ ): string {
108
+ const projectId = resolveProjectId(instance, options)
109
+ const {includeMembers, includeFeatures} = normalizeProjectOptions(options)
110
+ const membersKey = includeMembers ? ':members' : ''
111
+ const featuresKey = includeFeatures ? ':features' : ''
112
+ return `project:${projectId}${membersKey}${featuresKey}`
113
+ }
114
+
115
+ /**
116
+ * Fetcher for a single project (`GET /projects/:id`), on the shared fetcher
117
+ * cache. Pre-seeds from the default {@link projects} list entry when it can
118
+ * satisfy the read — the default list carries features but not members, so
119
+ * only `includeMembers: false` reads seed.
120
+ *
121
+ * @internal
122
+ */
123
+ export const project = defineFetcher<
124
+ [options?: ProjectOptions<boolean, boolean>],
125
+ Project<boolean, boolean>
126
+ >({
127
+ name: 'project',
128
+ getKey: getProjectCacheKey,
129
+ fetch: (instance) => (options) =>
130
+ getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
131
+ switchMap((client) =>
132
+ client.observable.request<Project<boolean, boolean>>({
133
+ uri: `/projects/${resolveProjectId(instance, options)}`,
134
+ query: buildQuery(normalizeProjectOptions(options)),
135
+ tag: 'projects.get',
136
+ }),
137
+ ),
138
+ ),
139
+ tags: (data) => [{type: 'project', id: data.id}],
140
+ initialData: (instance, options) => {
141
+ if (normalizeProjectOptions(options).includeMembers) return undefined
142
+ const snapshot = projects.getState(instance).getCurrent()
143
+ if (snapshot.status !== 'success') return undefined
144
+ const projectId = resolveProjectId(instance, options)
145
+ const match = snapshot.data.find((item) => item.id === projectId)
146
+ return match && {data: match, dataUpdatedAt: snapshot.dataUpdatedAt}
147
+ },
148
+ })
@@ -1,7 +1,6 @@
1
1
  import {NEVER} from 'rxjs'
2
2
  import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
3
3
 
4
- import {type DocumentResource} from '../config/sanityConfig'
5
4
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
5
  import {type StoreState} from '../store/createStoreState'
7
6
  import {hashString} from '../utils/hashString'
@@ -31,8 +30,7 @@ vi.mock('./subscribeToStateAndFetchBatches.ts')
31
30
 
32
31
  describe('getProjectionState', () => {
33
32
  let instance: SanityInstance
34
- const resource: DocumentResource = {projectId: 'p', dataset: 'd'}
35
- const docHandle = {documentId: 'exampleId', documentType: 'exampleType', resource}
33
+ const docHandle = {documentId: 'exampleId', documentType: 'exampleType'}
36
34
  const projection1 = '{exampleProjection1}'
37
35
  const hash1 = hashString(projection1)
38
36
  const projection2 = '{exampleProjection2}'
@@ -50,7 +48,7 @@ describe('getProjectionState', () => {
50
48
  return NEVER.subscribe()
51
49
  })
52
50
 
53
- instance = createSanityInstance()
51
+ instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
54
52
  vi.useFakeTimers() // Enable fake timers for each test
55
53
  })
56
54
 
@@ -128,7 +126,7 @@ describe('getProjectionState', () => {
128
126
  expect(state.get().subscriptions).toEqual({})
129
127
  expect(state.get().documentProjections).toEqual({})
130
128
 
131
- const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID 3
129
+ const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID 2
132
130
  expect(state.get().subscriptions).toEqual({
133
131
  [docHandle.documentId]: {[hash1]: {testSubId_2: true}},
134
132
  })
@@ -136,7 +134,7 @@ describe('getProjectionState', () => {
136
134
  [docHandle.documentId]: {[hash1]: projection1},
137
135
  })
138
136
 
139
- const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID 4
137
+ const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID 3
140
138
  expect(state.get().subscriptions).toEqual({
141
139
  [docHandle.documentId]: {
142
140
  [hash1]: {testSubId_2: true},
@@ -150,10 +148,12 @@ describe('getProjectionState', () => {
150
148
  },
151
149
  })
152
150
 
153
- const unsubscribe3 = projectionState1.subscribe(vi.fn()) // Should use ID 5
151
+ // should share ID 2 from the first subscription -- we only care if ANY subscription exists
152
+ // so we can include the document / projection in a bulk fetch
153
+ const unsubscribe3 = projectionState1.subscribe(vi.fn())
154
154
  expect(state.get().subscriptions).toEqual({
155
155
  [docHandle.documentId]: {
156
- [hash1]: {testSubId_2: true, testSubId_4: true},
156
+ [hash1]: {testSubId_2: true},
157
157
  [hash2]: {testSubId_3: true},
158
158
  },
159
159
  })
@@ -167,23 +167,20 @@ describe('getProjectionState', () => {
167
167
  })
168
168
 
169
169
  // --- Test Unsubscribe ---
170
- unsubscribe1() // Unsubscribes ID 3
171
- expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({
172
- testSubId_2: true,
173
- testSubId_4: true,
174
- })
170
+ unsubscribe1() // Unsubscribes first subscription consumer
171
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_2: true})
175
172
  vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
176
- expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_4: true})
173
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_2: true})
177
174
  expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toEqual(projection1)
178
175
 
179
- unsubscribe3() // Unsubscribes ID 5
176
+ unsubscribe3() // Also unsubscibes first projection subscription, should be removed
180
177
  vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
181
178
  expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toBeUndefined()
182
179
  expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toBeUndefined()
183
180
  expect(state.get().subscriptions[docHandle.documentId]?.[hash2]).toEqual({testSubId_3: true})
184
181
  expect(state.get().documentProjections[docHandle.documentId]?.[hash2]).toEqual(projection2)
185
182
 
186
- unsubscribe2() // Unsubscribes ID 4
183
+ unsubscribe2() // Unsubscribes second projection subscription, should be removed
187
184
  vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
188
185
  expect(state.get().subscriptions[docHandle.documentId]).toBeUndefined()
189
186
  expect(state.get().documentProjections[docHandle.documentId]).toBeUndefined()
@@ -205,14 +202,14 @@ describe('getProjectionState', () => {
205
202
  }))
206
203
 
207
204
  const unsubscribe1 = projectionState.subscribe(vi.fn()) // Should use ID 2
208
- const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should use ID 3
205
+ const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should reuse ID 2
209
206
 
210
207
  expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
211
208
  data: initialData,
212
209
  isPending: true,
213
210
  })
214
211
 
215
- unsubscribe1() // Unsubscribes ID 2
212
+ unsubscribe1() // Unsubscribes first subscription consumer
216
213
  vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
217
214
  expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
218
215
  data: initialData,
@@ -221,9 +218,10 @@ describe('getProjectionState', () => {
221
218
  expect(Object.keys(state.get().subscriptions[docHandle.documentId]?.[hash] ?? {}).length).toBe(
222
219
  1,
223
220
  )
224
- expect(state.get().subscriptions[docHandle.documentId]?.[hash]).toEqual({testSubId_3: true})
221
+ // should still have the same subscription ID since we're not unsubscribing the second consumer
222
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash]).toEqual({testSubId_2: true})
225
223
 
226
- unsubscribe2() // Unsubscribes ID 3
224
+ unsubscribe2() // Unsubscribes second subscription consumer
227
225
  expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
228
226
  data: initialData,
229
227
  isPending: true,
@@ -1,5 +1,5 @@
1
1
  import {DocumentId, getPublishedId} from '@sanity/id-utils'
2
- import {omit} from 'lodash-es'
2
+ import {type SanityProjectionResult} from 'groq'
3
3
 
4
4
  import {type DocumentHandle} from '../config/sanityConfig'
5
5
  import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
@@ -11,6 +11,8 @@ import {
11
11
  } from '../store/createStateSourceAction'
12
12
  import {hashString} from '../utils/hashString'
13
13
  import {insecureRandomId} from '../utils/ids'
14
+ import {omitProperty} from '../utils/object'
15
+ import {setCleanupTimeout} from '../utils/setCleanupTimeout'
14
16
  import {projectionStore} from './projectionStore'
15
17
  import {type ProjectionStoreState, type ProjectionValuePending} from './types'
16
18
  import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
@@ -24,6 +26,24 @@ export interface ProjectionOptions<
24
26
  projection: TProjection
25
27
  }
26
28
 
29
+ /**
30
+ * @beta
31
+ */
32
+ export function getProjectionState<
33
+ TProjection extends string = string,
34
+ TDocumentType extends string = string,
35
+ TDataset extends string = string,
36
+ TProjectId extends string = string,
37
+ >(
38
+ instance: SanityInstance,
39
+ options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
40
+ ): StateSource<
41
+ | ProjectionValuePending<
42
+ SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
43
+ >
44
+ | undefined
45
+ >
46
+
27
47
  /**
28
48
  * @beta
29
49
  */
@@ -91,9 +111,9 @@ export const _getProjectionState = bindActionByResourceAndPerspective(
91
111
  }))
92
112
 
93
113
  return () => {
94
- setTimeout(() => {
114
+ setCleanupTimeout(() => {
95
115
  state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
96
- const documentSubscriptionsForHash = omit(
116
+ const documentSubscriptionsForHash = omitProperty(
97
117
  prev.subscriptions[documentId]?.[projectionHash],
98
118
  subscriptionId,
99
119
  )
@@ -1,7 +1,7 @@
1
- import {DocumentId} from '@sanity/id-utils'
1
+ import {type ClientPerspective} from '@sanity/client'
2
+ import {DocumentId, getPublishedId} from '@sanity/id-utils'
2
3
 
3
- import {type PerspectiveHandle} from '../config/sanityConfig'
4
- import {getPublishedId} from '../utils/ids'
4
+ import {type ReleasePerspective} from '../config/sanityConfig'
5
5
  import {
6
6
  type DocumentProjections,
7
7
  type DocumentProjectionValues,
@@ -67,7 +67,7 @@ interface ProcessProjectionQueryOptions {
67
67
  ids: Set<string>
68
68
  results: ProjectionQueryResult[]
69
69
  documentStatuses?: ProjectionStoreState['documentStatuses']
70
- perspective: NonNullable<PerspectiveHandle['perspective']>
70
+ perspective: ClientPerspective | ReleasePerspective
71
71
  }
72
72
 
73
73
  export function processProjectionQuery({
@@ -84,7 +84,7 @@ export function processProjectionQuery({
84
84
  } = {}
85
85
 
86
86
  for (const result of results) {
87
- const originalId = getPublishedId(result._id)
87
+ const originalId = getPublishedId(DocumentId(result._id))
88
88
  const hash = result.__projectionHash
89
89
 
90
90
  if (!ids.has(originalId)) continue
@@ -6,14 +6,6 @@ import {createStoreInstance} from '../store/createStoreInstance'
6
6
  import {projectionStore} from './projectionStore'
7
7
  import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
8
8
 
9
- // Mock the module with a factory function
10
- vi.mock('../common/createLiveEventSubscriber', () => {
11
- const mockLiveSubscriber = vi.fn()
12
- return {
13
- createLiveEventSubscriber: vi.fn(() => mockLiveSubscriber),
14
- }
15
- })
16
-
17
9
  vi.mock('./subscribeToStateAndFetchBatches')
18
10
 
19
11
  describe('projectionStore', () => {
@@ -24,7 +16,7 @@ describe('projectionStore', () => {
24
16
  new Observable(subscriber).subscribe(),
25
17
  )
26
18
 
27
- const instance = createSanityInstance()
19
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
28
20
 
29
21
  const {state, dispose} = createStoreInstance(
30
22
  instance,
@@ -23,7 +23,7 @@ describe('resolveProjection', () => {
23
23
  } as ProjectionValuePending<Record<string, unknown>>),
24
24
  } as StateSource<ProjectionValuePending<Record<string, unknown>>>)
25
25
 
26
- instance = createSanityInstance()
26
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
27
27
  })
28
28
 
29
29
  afterEach(() => {
@@ -34,7 +34,6 @@ describe('resolveProjection', () => {
34
34
  const docHandle = createDocumentHandle({
35
35
  documentId: 'doc123',
36
36
  documentType: 'movie',
37
- resource: {projectId: 'p', dataset: 'd'},
38
37
  })
39
38
  const projection = '{title}'
40
39
 
@@ -1,3 +1,4 @@
1
+ import {type SanityProjectionResult} from 'groq'
1
2
  import {filter, firstValueFrom} from 'rxjs'
2
3
 
3
4
  import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
@@ -6,6 +7,21 @@ import {getProjectionState, type ProjectionOptions} from './getProjectionState'
6
7
  import {projectionStore} from './projectionStore'
7
8
  import {type ProjectionValuePending} from './types'
8
9
 
10
+ /** @beta */
11
+ export function resolveProjection<
12
+ TProjection extends string = string,
13
+ TDocumentType extends string = string,
14
+ TDataset extends string = string,
15
+ TProjectId extends string = string,
16
+ >(
17
+ instance: SanityInstance,
18
+ options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
19
+ ): Promise<
20
+ ProjectionValuePending<
21
+ SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
22
+ >
23
+ >
24
+
9
25
  /** @beta */
10
26
  export function resolveProjection<TData extends object>(
11
27
  instance: SanityInstance,
@@ -23,7 +23,7 @@ describe('subscribeToStateAndFetchBatches', () => {
23
23
 
24
24
  beforeEach(() => {
25
25
  vi.clearAllMocks()
26
- instance = createSanityInstance()
26
+ instance = createSanityInstance({projectId: 'test', dataset: 'test'})
27
27
  state = createStoreState<ProjectionStoreState>({
28
28
  documentProjections: {},
29
29
  documentStatuses: {},
@@ -1,4 +1,4 @@
1
- import {isEqual} from 'lodash-es'
1
+ import {DocumentId} from '@sanity/id-utils'
2
2
  import {
3
3
  combineLatest,
4
4
  debounceTime,
@@ -19,6 +19,7 @@ import {
19
19
  import {getQueryState, resolveQuery} from '../query/queryStore'
20
20
  import {type BoundPerspectiveKey} from '../store/createActionBinder'
21
21
  import {type StoreContext} from '../store/defineStore'
22
+ import {isDeepEqual} from '../utils/object'
22
23
  import {
23
24
  createProjectionQuery,
24
25
  processProjectionQuery,
@@ -45,18 +46,18 @@ export const subscribeToStateAndFetchBatches = ({
45
46
  }: StoreContext<ProjectionStoreState, BoundPerspectiveKey>): Subscription => {
46
47
  const documentProjections$ = state.observable.pipe(
47
48
  map((s) => s.documentProjections),
48
- distinctUntilChanged(isEqual),
49
+ distinctUntilChanged(isDeepEqual),
49
50
  )
50
51
 
51
52
  const activeDocumentIds$ = state.observable.pipe(
52
- map(({subscriptions}) => new Set(Object.keys(subscriptions))),
53
+ map(({subscriptions}) => new Set(Object.keys(subscriptions).map((id) => DocumentId(id)))),
53
54
  distinctUntilChanged(isSetEqual),
54
55
  )
55
56
 
56
57
  const pendingUpdateSubscription = activeDocumentIds$
57
58
  .pipe(
58
59
  debounceTime(BATCH_DEBOUNCE_TIME),
59
- startWith(new Set<string>()),
60
+ startWith(new Set<DocumentId>()),
60
61
  pairwise(),
61
62
  tap(([prevIds, currIds]) => {
62
63
  const newIds = [...currIds].filter((id) => !prevIds.has(id))
@@ -88,7 +89,7 @@ export const subscribeToStateAndFetchBatches = ({
88
89
 
89
90
  const queryTrigger$ = combineLatest([activeDocumentIds$, documentProjections$]).pipe(
90
91
  debounceTime(BATCH_DEBOUNCE_TIME),
91
- distinctUntilChanged(isEqual),
92
+ distinctUntilChanged(isDeepEqual),
92
93
  )
93
94
 
94
95
  const queryExecutionSubscription = queryTrigger$
@@ -105,10 +106,12 @@ export const subscribeToStateAndFetchBatches = ({
105
106
 
106
107
  const projectionQuery$ = new Observable<ProjectionQueryResult[]>((observer) => {
107
108
  const {getCurrent, observable} = getQueryState<ProjectionQueryResult[]>(instance, {
108
- query,
109
- params,
110
- tag: PROJECTION_TAG,
111
- perspective,
109
+ ...{
110
+ query,
111
+ params,
112
+ tag: PROJECTION_TAG,
113
+ perspective,
114
+ },
112
115
  resource,
113
116
  })
114
117
 
@@ -116,11 +119,13 @@ export const subscribeToStateAndFetchBatches = ({
116
119
  if (getCurrent() === undefined) {
117
120
  return from(
118
121
  resolveQuery<ProjectionQueryResult[]>(instance, {
119
- query,
120
- params,
121
- tag: PROJECTION_TAG,
122
- signal: controller.signal,
123
- perspective,
122
+ ...{
123
+ query,
124
+ params,
125
+ tag: PROJECTION_TAG,
126
+ signal: controller.signal,
127
+ perspective,
128
+ },
124
129
  resource,
125
130
  }),
126
131
  ).pipe(switchMap(() => observable))
@@ -140,10 +145,12 @@ export const subscribeToStateAndFetchBatches = ({
140
145
 
141
146
  const statusQuery$ = new Observable<StatusQueryResult[]>((observer) => {
142
147
  const {getCurrent, observable} = getQueryState<StatusQueryResult[]>(instance, {
143
- query: statusQuery,
144
- params: statusParams,
145
- tag: PROJECTION_TAG,
146
- perspective: 'raw',
148
+ ...{
149
+ query: statusQuery,
150
+ params: statusParams,
151
+ tag: PROJECTION_TAG,
152
+ perspective: 'raw',
153
+ },
147
154
  resource,
148
155
  })
149
156
 
@@ -151,11 +158,13 @@ export const subscribeToStateAndFetchBatches = ({
151
158
  if (getCurrent() === undefined) {
152
159
  return from(
153
160
  resolveQuery<StatusQueryResult[]>(instance, {
154
- query: statusQuery,
155
- params: statusParams,
156
- tag: PROJECTION_TAG,
157
- signal: controller.signal,
158
- perspective: 'raw',
161
+ ...{
162
+ query: statusQuery,
163
+ params: statusParams,
164
+ tag: PROJECTION_TAG,
165
+ signal: controller.signal,
166
+ perspective: 'raw',
167
+ },
159
168
  resource,
160
169
  }),
161
170
  ).pipe(switchMap(() => observable))
@@ -0,0 +1,32 @@
1
+ import {expectTypeOf, test} from 'vitest'
2
+
3
+ import {type Project, type ProjectMember} from '../project/project'
4
+ import {type SanityInstance} from '../store/createSanityInstance'
5
+ import {type StateSource} from '../store/createStateSourceAction'
6
+ import {type FetcherSnapshot} from '../store/fetcherStore'
7
+ import {projects} from './projects'
8
+
9
+ const instance = {} as SanityInstance
10
+
11
+ test('projects.resolveState — resolves the wide-boolean list shape', () => {
12
+ expectTypeOf(projects.resolveState(instance)).resolves.toEqualTypeOf<
13
+ Project<boolean, boolean>[]
14
+ >()
15
+ })
16
+
17
+ test('Project<boolean, boolean> — members and features are optional', () => {
18
+ type Item = Project<boolean, boolean>
19
+ expectTypeOf<Item['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
20
+ expectTypeOf<Item['features']>().toEqualTypeOf<string[] | undefined>()
21
+ })
22
+
23
+ test('projects.getState — exposes the snapshot envelope', () => {
24
+ expectTypeOf(projects.getState(instance)).toEqualTypeOf<
25
+ StateSource<FetcherSnapshot<Project<boolean, boolean>[]>>
26
+ >()
27
+ })
28
+
29
+ test('projects — rejects non-boolean flag values', () => {
30
+ // @ts-expect-error includeMembers must be a boolean
31
+ void projects.resolveState(instance, {includeMembers: 'yes'})
32
+ })