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

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 (197) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
  3. package/dist/_chunks-es/_internal.js +134 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  7. package/dist/_chunks-es/telemetryManager.js +93 -0
  8. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  9. package/dist/_exports/_internal.d.ts +95 -0
  10. package/dist/_exports/_internal.d.ts.map +1 -0
  11. package/dist/_exports/_internal.js +24 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +1079 -1076
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +1967 -2302
  16. package/dist/index.js.map +1 -1
  17. package/package.json +48 -47
  18. package/src/_exports/_internal.ts +26 -0
  19. package/src/_exports/index.ts +120 -11
  20. package/src/access/checkPermissions.test.ts +55 -0
  21. package/src/access/checkPermissions.ts +54 -0
  22. package/src/agent/agentActions.test.ts +16 -60
  23. package/src/agent/agentActions.ts +25 -38
  24. package/src/applications/applications.test-d.ts +83 -0
  25. package/src/applications/applications.test.ts +130 -0
  26. package/src/applications/applications.ts +364 -0
  27. package/src/applications/userApplications.test.ts +126 -0
  28. package/src/applications/userApplications.ts +261 -0
  29. package/src/auth/authLogger.ts +30 -0
  30. package/src/auth/authMode.test.ts +25 -0
  31. package/src/auth/authMode.ts +6 -3
  32. package/src/auth/authStore.test.ts +194 -13
  33. package/src/auth/authStore.ts +59 -26
  34. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  35. package/src/auth/getOrganizationVerificationState.ts +4 -4
  36. package/src/auth/handleAuthCallback.test.ts +35 -1
  37. package/src/auth/handleAuthCallback.ts +25 -6
  38. package/src/auth/logout.test.ts +75 -2
  39. package/src/auth/logout.ts +23 -4
  40. package/src/auth/refreshStampedToken.test.ts +219 -469
  41. package/src/auth/refreshStampedToken.ts +79 -195
  42. package/src/auth/studioAuth.ts +1 -1
  43. package/src/auth/studioModeAuth.ts +2 -1
  44. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  45. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  46. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  47. package/src/client/clientStore.test.ts +62 -37
  48. package/src/client/clientStore.ts +45 -75
  49. package/src/client/liveEvents.test.ts +151 -0
  50. package/src/client/liveEvents.ts +107 -0
  51. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  52. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  53. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  54. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  55. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  56. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  57. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  58. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  59. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  60. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  61. package/src/comlink/node/getNodeState.test.ts +1 -1
  62. package/src/comlink/node/getNodeState.ts +2 -1
  63. package/src/config/__tests__/handles.test.ts +18 -12
  64. package/src/config/handles.ts +25 -7
  65. package/src/config/sanityConfig.ts +109 -79
  66. package/src/datasets/datasets.test.ts +2 -2
  67. package/src/datasets/datasets.ts +10 -4
  68. package/src/document/actions.test.ts +115 -33
  69. package/src/document/actions.ts +212 -15
  70. package/src/document/applyDocumentActions.test.ts +38 -12
  71. package/src/document/applyDocumentActions.ts +30 -9
  72. package/src/document/documentConstants.ts +7 -0
  73. package/src/document/documentStore.concurrency.test.ts +1306 -0
  74. package/src/document/documentStore.test.ts +673 -278
  75. package/src/document/documentStore.ts +162 -71
  76. package/src/document/events.test.ts +57 -2
  77. package/src/document/events.ts +82 -26
  78. package/src/document/listen.test.ts +56 -0
  79. package/src/document/listen.ts +74 -21
  80. package/src/document/listenerEventOperators.test.ts +311 -0
  81. package/src/document/listenerEventOperators.ts +217 -0
  82. package/src/document/patchOperations.test.ts +49 -2
  83. package/src/document/patchOperations.ts +50 -0
  84. package/src/document/permissions.test.ts +105 -60
  85. package/src/document/permissions.ts +44 -19
  86. package/src/document/processActions/create.ts +138 -0
  87. package/src/document/processActions/delete.ts +100 -0
  88. package/src/document/processActions/discard.ts +63 -0
  89. package/src/document/processActions/edit.ts +154 -0
  90. package/src/document/processActions/processActions.ts +218 -0
  91. package/src/document/processActions/publish.ts +120 -0
  92. package/src/document/processActions/releaseArchive.ts +77 -0
  93. package/src/document/processActions/releaseCreate.ts +59 -0
  94. package/src/document/processActions/releaseDelete.ts +65 -0
  95. package/src/document/processActions/releaseEdit.ts +37 -0
  96. package/src/document/processActions/releasePublish.ts +45 -0
  97. package/src/document/processActions/releaseSchedule.ts +87 -0
  98. package/src/document/processActions/releaseUtil.ts +31 -0
  99. package/src/document/processActions/shared.ts +308 -0
  100. package/src/document/processActions/unpublish.ts +85 -0
  101. package/src/document/processActions.test.ts +1129 -121
  102. package/src/document/reducers.test.ts +639 -64
  103. package/src/document/reducers.ts +187 -48
  104. package/src/document/resourceRules.test.ts +178 -0
  105. package/src/document/resourceRules.ts +117 -0
  106. package/src/document/sharedListener.test.ts +16 -14
  107. package/src/document/sharedListener.ts +24 -8
  108. package/src/document/util.ts +14 -0
  109. package/src/favorites/favorites.test.ts +5 -6
  110. package/src/installations/installations.test-d.ts +68 -0
  111. package/src/installations/installations.test.ts +62 -0
  112. package/src/installations/installations.ts +202 -0
  113. package/src/organization/organization.test-d.ts +36 -0
  114. package/src/organization/organization.test.ts +138 -0
  115. package/src/organization/organization.ts +138 -0
  116. package/src/organizations/organizations.test-d.ts +56 -0
  117. package/src/organizations/organizations.test.ts +150 -0
  118. package/src/organizations/organizations.ts +107 -0
  119. package/src/presence/bifurTransport.test.ts +46 -6
  120. package/src/presence/bifurTransport.ts +19 -2
  121. package/src/presence/presenceStore.test.ts +94 -23
  122. package/src/presence/presenceStore.ts +85 -29
  123. package/src/preview/getPreviewState.test.ts +137 -0
  124. package/src/preview/getPreviewState.ts +69 -0
  125. package/src/preview/previewProjectionUtils.test.ts +9 -22
  126. package/src/preview/previewProjectionUtils.ts +7 -3
  127. package/src/preview/resolvePreview.test.ts +64 -0
  128. package/src/preview/resolvePreview.ts +43 -0
  129. package/src/preview/types.ts +10 -0
  130. package/src/project/project.test-d.ts +37 -0
  131. package/src/project/project.test.ts +157 -12
  132. package/src/project/project.ts +138 -25
  133. package/src/projection/getProjectionState.test.ts +18 -20
  134. package/src/projection/getProjectionState.ts +23 -3
  135. package/src/projection/projectionQuery.ts +5 -5
  136. package/src/projection/projectionStore.test.ts +1 -1
  137. package/src/projection/resolveProjection.test.ts +1 -2
  138. package/src/projection/resolveProjection.ts +16 -0
  139. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  140. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  141. package/src/projection/types.ts +8 -0
  142. package/src/projects/projects.test-d.ts +32 -0
  143. package/src/projects/projects.test.ts +108 -42
  144. package/src/projects/projects.ts +63 -27
  145. package/src/query/queryStore.test.ts +259 -123
  146. package/src/query/queryStore.ts +78 -63
  147. package/src/query/reducers.test.ts +0 -42
  148. package/src/query/reducers.ts +2 -18
  149. package/src/releases/getPerspectiveState.test.ts +17 -23
  150. package/src/releases/getPerspectiveState.ts +8 -20
  151. package/src/releases/observeReleases.test.ts +127 -0
  152. package/src/releases/observeReleases.ts +102 -0
  153. package/src/releases/releasesStore.test.ts +100 -53
  154. package/src/releases/releasesStore.ts +69 -44
  155. package/src/releases/utils/sortReleases.test.ts +38 -2
  156. package/src/releases/utils/sortReleases.ts +15 -10
  157. package/src/store/createActionBinder.test.ts +87 -90
  158. package/src/store/createActionBinder.ts +79 -25
  159. package/src/store/createSanityInstance.test.ts +83 -13
  160. package/src/store/createSanityInstance.ts +77 -2
  161. package/src/store/createStateSourceAction.test.ts +64 -2
  162. package/src/store/createStateSourceAction.ts +39 -44
  163. package/src/store/createStoreInstance.test.ts +4 -2
  164. package/src/store/fetcherStore.test-d.ts +121 -0
  165. package/src/store/fetcherStore.test.ts +299 -0
  166. package/src/store/fetcherStore.ts +801 -0
  167. package/src/telemetry/environment.test.ts +119 -0
  168. package/src/telemetry/environment.ts +92 -0
  169. package/src/telemetry/events.ts +42 -0
  170. package/src/telemetry/initTelemetry.test.ts +432 -0
  171. package/src/telemetry/initTelemetry.ts +218 -0
  172. package/src/telemetry/telemetryManager.test.ts +327 -0
  173. package/src/telemetry/telemetryManager.ts +199 -0
  174. package/src/users/reducers.test.ts +6 -1
  175. package/src/users/reducers.ts +5 -6
  176. package/src/users/types.ts +4 -4
  177. package/src/users/usersStore.test.ts +16 -12
  178. package/src/users/usersStore.ts +5 -1
  179. package/src/utils/buildQuery.test.ts +14 -0
  180. package/src/utils/buildQuery.ts +17 -0
  181. package/src/utils/createFetcherStore.test.ts +7 -5
  182. package/src/utils/createFetcherStore.ts +8 -5
  183. package/src/utils/getStagingApiHost.test.ts +21 -0
  184. package/src/utils/getStagingApiHost.ts +14 -0
  185. package/src/utils/ids.test.ts +1 -29
  186. package/src/utils/ids.ts +0 -10
  187. package/src/utils/includeShape.ts +36 -0
  188. package/src/utils/isImportError.test.ts +72 -0
  189. package/src/utils/isImportError.ts +34 -0
  190. package/src/utils/logger.test.ts +12 -0
  191. package/src/utils/logger.ts +8 -3
  192. package/src/utils/object.test.ts +95 -0
  193. package/src/utils/object.ts +142 -0
  194. package/src/utils/setCleanupTimeout.ts +37 -0
  195. package/src/document/processActions.ts +0 -707
  196. package/src/utils/defineIntent.test.ts +0 -477
  197. package/src/utils/defineIntent.ts +0 -244
@@ -1,37 +1,38 @@
1
- // NOTE: vi.mock REMOVED
2
-
3
1
  import {of, Subscription, throwError} from 'rxjs'
4
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest' // Removed Mock type
2
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
5
3
 
6
4
  import {createSanityInstance} from '../store/createSanityInstance'
7
5
  import {createStoreState} from '../store/createStoreState'
8
6
  import {AuthStateType} from './authStateType'
9
7
  import {type AuthState, authStore} from './authStore'
10
- import {
11
- getLastRefreshTime,
12
- getNextRefreshDelay,
13
- refreshStampedToken,
14
- setLastRefreshTime,
15
- } from './refreshStampedToken'
8
+ import {refreshStampedToken} from './refreshStampedToken'
16
9
  import {createLoggedInAuthState} from './utils'
17
10
 
18
- // Type definitions for Web Locks (can be kept if needed for context)
19
- // ... (Lock, LockOptions, LockGrantedCallback types)
11
+ // Mock logger to prevent actual logging during tests
12
+ vi.mock('../utils/logger', async (importOriginal) => {
13
+ const original = await importOriginal<typeof import('../utils/logger')>()
14
+ return {
15
+ ...original,
16
+ createLogger: vi.fn(() => ({
17
+ info: vi.fn(),
18
+ debug: vi.fn(),
19
+ warn: vi.fn(),
20
+ error: vi.fn(),
21
+ trace: vi.fn(),
22
+ })),
23
+ }
24
+ })
20
25
 
21
26
  describe('refreshStampedToken', () => {
22
27
  let mockStorage: Storage
23
- let originalNavigator: typeof navigator // Restored
24
28
  let originalDocument: Document
25
29
  let subscriptions: Subscription[]
26
- // mockLocksRequest removed
27
30
 
28
31
  beforeEach(() => {
29
32
  subscriptions = []
30
33
  vi.clearAllMocks()
31
34
  vi.useFakeTimers()
32
35
 
33
- originalNavigator = global.navigator // Restore original navigator setup
34
-
35
36
  // Mock document for visibility API
36
37
  originalDocument = global.document
37
38
  Object.defineProperty(global, 'document', {
@@ -52,49 +53,10 @@ describe('refreshStampedToken', () => {
52
53
  key: vi.fn(),
53
54
  length: 0,
54
55
  }
55
- // Restore a basic, functional mock for navigator.locks
56
- // This mock *will* run the callback, including the infinite loop
57
- // if not handled carefully in tests.
58
- const mockLocks = {
59
- request: vi.fn(
60
- async (
61
- _name: string,
62
- _options: LockOptions | LockGrantedCallback,
63
- callback?: LockGrantedCallback,
64
- ) => {
65
- const actualCallback = typeof _options === 'function' ? _options : callback
66
- if (!actualCallback) return false
67
- const mockLock: Lock = {name: 'mock-lock', mode: 'exclusive'}
68
- try {
69
- await new Promise((resolve) => setTimeout(resolve, 0))
70
- // CAUTION: This executes the callback provided by acquireTokenRefreshLock
71
- // which contains the infinite loop. Tests need to avoid triggering
72
- // timers indefinitely if this runs.
73
- await actualCallback(mockLock)
74
- // This return is unlikely to be hit if callback loops
75
- return true
76
- } catch (error) {
77
- // eslint-disable-next-line no-console
78
- console.error('Mock lock request failed:', error)
79
- return false
80
- }
81
- },
82
- ),
83
- query: vi.fn(async () => ({held: [], pending: []})),
84
- }
85
- Object.defineProperty(global, 'navigator', {
86
- value: {locks: mockLocks},
87
- writable: true,
88
- })
89
56
  })
90
57
 
91
58
  afterEach(async () => {
92
59
  subscriptions.forEach((sub) => sub.unsubscribe())
93
- // Restore original navigator
94
- Object.defineProperty(global, 'navigator', {
95
- value: originalNavigator,
96
- writable: true,
97
- })
98
60
  // Restore original document
99
61
  Object.defineProperty(global, 'document', {
100
62
  value: originalDocument,
@@ -110,351 +72,229 @@ describe('refreshStampedToken', () => {
110
72
  vi.useRealTimers()
111
73
  })
112
74
 
113
- describe('dashboard context', () => {
114
- it('refreshes the token immediately without using locks', async () => {
115
- // Test setup remains similar, using fake timers
116
- const mockClient = {
117
- observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
118
- }
119
- const mockClientFactory = vi.fn().mockReturnValue(mockClient)
120
- const instance = createSanityInstance({
121
- auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
122
- })
123
- const initialState = authStore.getInitialState(instance, null)
124
- initialState.authState = {
125
- type: AuthStateType.LOGGED_IN,
126
- token: 'sk-initial-token-st123',
127
- currentUser: null,
128
- }
129
- initialState.dashboardContext = {mode: 'test'}
130
- const state = createStoreState(initialState)
131
-
132
- const subscription = refreshStampedToken({state, instance, key: null})
133
- subscriptions.push(subscription)
134
-
135
- await vi.advanceTimersToNextTimerAsync()
136
-
137
- const finalAuthStateDash = state.get().authState
138
- expect(finalAuthStateDash.type).toBe(AuthStateType.LOGGED_IN)
139
- // Ensure token was updated
140
- if (finalAuthStateDash.type === AuthStateType.LOGGED_IN) {
141
- expect(finalAuthStateDash.token).toBe('sk-refreshed-token-st123')
142
- }
143
- // Verify navigator.locks.request was NOT called
144
- const locksRequest = navigator.locks.request as ReturnType<typeof vi.fn>
145
- expect(locksRequest).not.toHaveBeenCalled()
75
+ it('refreshes the token when the interval timer fires', async () => {
76
+ const mockClient = {
77
+ observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
78
+ }
79
+ const mockClientFactory = vi.fn().mockReturnValue(mockClient)
80
+ const instance = createSanityInstance({
81
+ projectId: 'p',
82
+ dataset: 'd',
83
+ auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
146
84
  })
85
+ const initialState = authStore.getInitialState(instance, null)
86
+ initialState.authState = {
87
+ type: AuthStateType.LOGGED_IN,
88
+ token: 'sk-initial-token-st123',
89
+ currentUser: null,
90
+ }
91
+ const state = createStoreState(initialState)
92
+
93
+ const subscription = refreshStampedToken({state, instance, key: null})
94
+ subscriptions.push(subscription)
95
+
96
+ await vi.advanceTimersToNextTimerAsync()
97
+
98
+ const finalAuthState = state.get().authState
99
+ expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
100
+ if (finalAuthState.type === AuthStateType.LOGGED_IN) {
101
+ expect(finalAuthState.token).toBe('sk-refreshed-token-st123')
102
+ }
103
+ })
104
+
105
+ it('writes the refreshed token to storage', async () => {
106
+ const mockClient = {
107
+ observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
108
+ }
109
+ const mockClientFactory = vi.fn().mockReturnValue(mockClient)
110
+ const instance = createSanityInstance({
111
+ projectId: 'p',
112
+ dataset: 'd',
113
+ auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
114
+ })
115
+ const initialState = authStore.getInitialState(instance, null)
116
+ initialState.authState = {
117
+ type: AuthStateType.LOGGED_IN,
118
+ token: 'sk-initial-token-st123',
119
+ currentUser: null,
120
+ }
121
+ const state = createStoreState(initialState)
122
+
123
+ const subscription = refreshStampedToken({state, instance, key: null})
124
+ subscriptions.push(subscription)
125
+
126
+ await vi.advanceTimersToNextTimerAsync()
127
+
128
+ expect(mockClient.observable.request).toHaveBeenCalled()
129
+ expect(mockStorage.setItem).toHaveBeenCalledWith(
130
+ initialState.options.storageKey,
131
+ JSON.stringify({token: 'sk-refreshed-token-st123'}),
132
+ )
133
+ })
147
134
 
148
- it('does not refresh on visibility change when lastTokenRefresh is recent', async () => {
149
- const mockClient = {
150
- observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
151
- }
152
- const mockClientFactory = vi.fn().mockReturnValue(mockClient)
153
- const instance = createSanityInstance({
154
- defaultResource: {projectId: 'p', dataset: 'd'},
155
- auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
156
- })
157
- const initialState = authStore.getInitialState(instance, null)
158
- initialState.authState = createLoggedInAuthState('sk-initial-token-st123', null)
159
- initialState.dashboardContext = {mode: 'test'}
160
- const state = createStoreState(initialState)
161
-
162
- const subscription = refreshStampedToken({state, instance, key: null})
163
- subscriptions.push(subscription)
164
-
165
- const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
166
- expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
167
- const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
168
-
169
- Object.defineProperty(global.document, 'visibilityState', {
170
- value: 'visible',
171
- writable: true,
172
- configurable: true,
173
- })
174
-
175
- visibilityHandler()
176
- await vi.advanceTimersByTimeAsync(100)
177
-
178
- expect(mockClient.observable.request).not.toHaveBeenCalled()
179
- const finalAuthState = state.get().authState
180
- if (finalAuthState.type === AuthStateType.LOGGED_IN) {
181
- expect(finalAuthState.token).toBe('sk-initial-token-st123')
182
- }
135
+ it('does not refresh on visibility change when lastTokenRefresh is recent', async () => {
136
+ const mockClient = {
137
+ observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
138
+ }
139
+ const mockClientFactory = vi.fn().mockReturnValue(mockClient)
140
+ const instance = createSanityInstance({
141
+ auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
183
142
  })
143
+ const initialState = authStore.getInitialState(instance, null)
144
+ initialState.authState = createLoggedInAuthState('sk-initial-token-st123', null)
145
+ const state = createStoreState(initialState)
146
+
147
+ const subscription = refreshStampedToken({state, instance, key: null})
148
+ subscriptions.push(subscription)
184
149
 
185
- it('refreshes on visibility change when lastTokenRefresh is stale', async () => {
186
- const REFRESH_INTERVAL = 12 * 60 * 60 * 1000
187
- const mockClient = {
188
- observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
189
- }
190
- const mockClientFactory = vi.fn().mockReturnValue(mockClient)
191
- const instance = createSanityInstance({
192
- defaultResource: {projectId: 'p', dataset: 'd'},
193
- auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
194
- })
195
- const initialState = authStore.getInitialState(instance, null)
196
- const staleTimestamp = Date.now() - REFRESH_INTERVAL - 1000
197
- initialState.authState = {
198
- type: AuthStateType.LOGGED_IN,
199
- token: 'sk-initial-token-st123',
200
- currentUser: null,
201
- lastTokenRefresh: staleTimestamp,
202
- }
203
- initialState.dashboardContext = {mode: 'test'}
204
- const state = createStoreState(initialState)
205
-
206
- const subscription = refreshStampedToken({state, instance, key: null})
207
- subscriptions.push(subscription)
208
-
209
- const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
210
- expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
211
- const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
212
-
213
- Object.defineProperty(global.document, 'visibilityState', {
214
- value: 'visible',
215
- writable: true,
216
- configurable: true,
217
- })
218
-
219
- visibilityHandler()
220
- await vi.advanceTimersToNextTimerAsync()
221
-
222
- expect(mockClient.observable.request).toHaveBeenCalled()
223
- const finalAuthState = state.get().authState
224
- if (finalAuthState.type === AuthStateType.LOGGED_IN) {
225
- expect(finalAuthState.token).toBe('sk-refreshed-token-st123')
226
- expect(finalAuthState.lastTokenRefresh).toBeGreaterThan(staleTimestamp)
227
- }
150
+ const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
151
+ expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
152
+ const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
153
+
154
+ Object.defineProperty(global.document, 'visibilityState', {
155
+ value: 'visible',
156
+ writable: true,
157
+ configurable: true,
228
158
  })
229
159
 
230
- it('refreshes on visibility change when lastTokenRefresh is undefined (pre-fix behavior)', async () => {
231
- const mockClient = {
232
- observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
233
- }
234
- const mockClientFactory = vi.fn().mockReturnValue(mockClient)
235
- const instance = createSanityInstance({
236
- defaultResource: {projectId: 'p', dataset: 'd'},
237
- auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
238
- })
239
- const initialState = authStore.getInitialState(instance, null)
240
- initialState.authState = {
241
- type: AuthStateType.LOGGED_IN,
242
- token: 'sk-initial-token-st123',
243
- currentUser: null,
244
- // lastTokenRefresh intentionally omitted to demonstrate the old bug
245
- }
246
- initialState.dashboardContext = {mode: 'test'}
247
- const state = createStoreState(initialState)
248
-
249
- const subscription = refreshStampedToken({state, instance, key: null})
250
- subscriptions.push(subscription)
251
-
252
- const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
253
- const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
254
-
255
- Object.defineProperty(global.document, 'visibilityState', {
256
- value: 'visible',
257
- writable: true,
258
- configurable: true,
259
- })
260
-
261
- visibilityHandler()
262
- await vi.advanceTimersToNextTimerAsync()
263
-
264
- // Without lastTokenRefresh, shouldRefreshToken returns true — this is the bug
265
- expect(mockClient.observable.request).toHaveBeenCalled()
160
+ visibilityHandler()
161
+ await vi.advanceTimersByTimeAsync(100)
162
+
163
+ expect(mockClient.observable.request).not.toHaveBeenCalled()
164
+ const finalAuthState = state.get().authState
165
+ if (finalAuthState.type === AuthStateType.LOGGED_IN) {
166
+ expect(finalAuthState.token).toBe('sk-initial-token-st123')
167
+ }
168
+ })
169
+
170
+ it('refreshes on visibility change when lastTokenRefresh is stale', async () => {
171
+ const REFRESH_INTERVAL = 12 * 60 * 60 * 1000
172
+ const mockClient = {
173
+ observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
174
+ }
175
+ const mockClientFactory = vi.fn().mockReturnValue(mockClient)
176
+ const instance = createSanityInstance({
177
+ auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
266
178
  })
179
+ const initialState = authStore.getInitialState(instance, null)
180
+ const staleTimestamp = Date.now() - REFRESH_INTERVAL - 1000
181
+ initialState.authState = {
182
+ type: AuthStateType.LOGGED_IN,
183
+ token: 'sk-initial-token-st123',
184
+ currentUser: null,
185
+ lastTokenRefresh: staleTimestamp,
186
+ }
187
+ const state = createStoreState(initialState)
267
188
 
268
- it('does not refresh when tab is not visible', async () => {
269
- // Set visibility to hidden
270
- Object.defineProperty(global, 'document', {
271
- value: {
272
- visibilityState: 'hidden',
273
- addEventListener: vi.fn(),
274
- removeEventListener: vi.fn(),
275
- },
276
- writable: true,
277
- configurable: true,
278
- })
279
-
280
- const mockClient = {
281
- observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
282
- }
283
- const mockClientFactory = vi.fn().mockReturnValue(mockClient)
284
- const instance = createSanityInstance({
285
- auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
286
- })
287
- const initialState = authStore.getInitialState(instance, null)
288
- initialState.authState = {
289
- type: AuthStateType.LOGGED_IN,
290
- token: 'sk-initial-token-st123',
291
- currentUser: null,
292
- }
293
- initialState.dashboardContext = {mode: 'test'}
294
- const state = createStoreState(initialState)
295
-
296
- const subscription = refreshStampedToken({state, instance, key: null})
297
- subscriptions.push(subscription)
298
-
299
- await vi.advanceTimersToNextTimerAsync()
300
-
301
- // Verify that no refresh occurred
302
- expect(mockClient.observable.request).not.toHaveBeenCalled()
303
- const finalAuthState = state.get().authState
304
- if (finalAuthState.type === AuthStateType.LOGGED_IN) {
305
- expect(finalAuthState.token).toBe('sk-initial-token-st123')
306
- }
189
+ const subscription = refreshStampedToken({state, instance, key: null})
190
+ subscriptions.push(subscription)
191
+
192
+ const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
193
+ expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
194
+ const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
195
+
196
+ Object.defineProperty(global.document, 'visibilityState', {
197
+ value: 'visible',
198
+ writable: true,
199
+ configurable: true,
307
200
  })
201
+
202
+ visibilityHandler()
203
+ await vi.advanceTimersToNextTimerAsync()
204
+
205
+ expect(mockClient.observable.request).toHaveBeenCalled()
206
+ const finalAuthState = state.get().authState
207
+ if (finalAuthState.type === AuthStateType.LOGGED_IN) {
208
+ expect(finalAuthState.token).toBe('sk-refreshed-token-st123')
209
+ expect(finalAuthState.lastTokenRefresh).toBeGreaterThan(staleTimestamp)
210
+ }
308
211
  })
309
212
 
310
- describe('non-dashboard context', () => {
311
- // Test is simplified: just ensure it runs without error
312
- it('attempts token refresh coordination when not in dashboard context', async () => {
313
- // Fake timers enabled via beforeEach
314
- const mockClient = {observable: {request: vi.fn()}}
315
- const mockClientFactory = vi.fn().mockReturnValue(mockClient)
316
- const instance = createSanityInstance({
317
- auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
318
- })
319
- const initialState = authStore.getInitialState(instance, null)
320
- initialState.authState = {
321
- type: AuthStateType.LOGGED_IN,
322
- token: 'sk-initial-token-st123',
323
- currentUser: null,
324
- }
325
- const state = createStoreState(initialState)
326
-
327
- let subscription: Subscription | undefined
328
- // We expect this NOT to throw, but accept we can't easily test the lock call or outcome
329
- expect(() => {
330
- subscription = refreshStampedToken({state, instance, key: null})
331
- subscriptions.push(subscription!)
332
- }).not.toThrow()
333
-
334
- // DO NOT advance timers or yield here - focus on immediate observable logic
335
- // We cannot reliably test that failingLocksRequest is called due to async/timer issues,
336
- // but we *can* test the consequence of it resolving to false.
337
-
338
- // VERIFY THE OUTCOME:
339
- // Check client request was NOT made (because filter(hasLock => hasLock) receives false)
340
- expect(mockClient.observable.request).not.toHaveBeenCalled()
341
- // Check state remains unchanged
342
- const finalAuthState = state.get().authState
343
- expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
344
- if (finalAuthState.type === AuthStateType.LOGGED_IN) {
345
- expect(finalAuthState.token).toBe('sk-initial-token-st123')
346
- }
213
+ it('refreshes on visibility change when lastTokenRefresh is undefined (pre-fix behavior)', async () => {
214
+ const mockClient = {
215
+ observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
216
+ }
217
+ const mockClientFactory = vi.fn().mockReturnValue(mockClient)
218
+ const instance = createSanityInstance({
219
+ auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
347
220
  })
221
+ const initialState = authStore.getInitialState(instance, null)
222
+ initialState.authState = {
223
+ type: AuthStateType.LOGGED_IN,
224
+ token: 'sk-initial-token-st123',
225
+ currentUser: null,
226
+ // lastTokenRefresh intentionally omitted to demonstrate the old bug
227
+ }
228
+ const state = createStoreState(initialState)
229
+
230
+ const subscription = refreshStampedToken({state, instance, key: null})
231
+ subscriptions.push(subscription)
232
+
233
+ const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
234
+ const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
348
235
 
349
- it('skips refresh if lock request returns false', async () => {
350
- // Fake timers enabled via beforeEach
351
- // Mock navigator.locks.request LOCALLY for this test to return false
352
- const originalLocks = navigator.locks
353
- // Use mockResolvedValue: the from(acquireTokenRefreshLock(...)) expects a Promise
354
- const failingLocksRequest = vi.fn().mockResolvedValue(false)
355
- Object.defineProperty(global, 'navigator', {
356
- value: {locks: {...originalLocks, request: failingLocksRequest}},
357
- writable: true,
358
- })
359
-
360
- try {
361
- const mockClient = {
362
- observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
363
- }
364
- const mockClientFactory = vi.fn().mockReturnValue(mockClient)
365
- const instance = createSanityInstance({
366
- auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
367
- })
368
- const initialState = authStore.getInitialState(instance, null)
369
- initialState.authState = {
370
- type: AuthStateType.LOGGED_IN,
371
- token: 'sk-initial-token-st123',
372
- currentUser: null,
373
- }
374
- const state = createStoreState(initialState)
375
-
376
- const subscription = refreshStampedToken({state, instance, key: null})
377
- subscriptions.push(subscription)
378
-
379
- // DO NOT advance timers or yield here - focus on immediate observable logic
380
- // We cannot reliably test that failingLocksRequest is called due to async/timer issues,
381
- // but we *can* test the consequence of it resolving to false.
382
-
383
- // VERIFY THE OUTCOME:
384
- // Check client request was NOT made (because filter(hasLock => hasLock) receives false)
385
- expect(mockClient.observable.request).not.toHaveBeenCalled()
386
- // Check state remains unchanged
387
- const finalAuthState = state.get().authState
388
- expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
389
- if (finalAuthState.type === AuthStateType.LOGGED_IN) {
390
- expect(finalAuthState.token).toBe('sk-initial-token-st123')
391
- }
392
- } finally {
393
- // Restore original navigator.locks
394
- Object.defineProperty(global, 'navigator', {value: {locks: originalLocks}, writable: true})
395
- }
236
+ Object.defineProperty(global.document, 'visibilityState', {
237
+ value: 'visible',
238
+ writable: true,
239
+ configurable: true,
396
240
  })
241
+
242
+ visibilityHandler()
243
+ await vi.advanceTimersToNextTimerAsync()
244
+
245
+ // Without lastTokenRefresh, shouldRefreshToken returns true — this is the bug
246
+ expect(mockClient.observable.request).toHaveBeenCalled()
397
247
  })
398
248
 
399
- describe('unsupported environments', () => {
400
- it('falls back to immediate refresh if Web Locks API is not supported', async () => {
401
- // Temporarily remove navigator.locks for this test
402
- const originalLocks = navigator.locks
403
- Object.defineProperty(global.navigator, 'locks', {
404
- value: undefined,
405
- writable: true,
406
- })
407
-
408
- try {
409
- const mockClient = {
410
- observable: {request: vi.fn(() => of({token: 'sk-refreshed-immediately-st123'}))},
411
- }
412
- const mockClientFactory = vi.fn().mockReturnValue(mockClient)
413
- const instance = createSanityInstance({
414
- auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
415
- })
416
- const initialState = authStore.getInitialState(instance, null)
417
- initialState.authState = {
418
- type: AuthStateType.LOGGED_IN,
419
- token: 'sk-initial-token-st123',
420
- currentUser: null,
421
- }
422
- const state = createStoreState(initialState)
423
-
424
- const subscription = refreshStampedToken({state, instance, key: null})
425
- subscriptions.push(subscription)
426
-
427
- // Advance timers to allow the async `performRefresh` to execute
428
- await vi.advanceTimersToNextTimerAsync()
429
-
430
- // Verify the refresh was performed and state was updated
431
- expect(mockClient.observable.request).toHaveBeenCalled()
432
- const finalAuthState = state.get().authState
433
- expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
434
- if (finalAuthState.type === AuthStateType.LOGGED_IN) {
435
- expect(finalAuthState.token).toBe('sk-refreshed-immediately-st123')
436
- }
437
- // Verify token was set in storage
438
- expect(mockStorage.setItem).toHaveBeenCalledWith(
439
- initialState.options.storageKey,
440
- JSON.stringify({token: 'sk-refreshed-immediately-st123'}),
441
- )
442
- } finally {
443
- // Restore navigator.locks
444
- Object.defineProperty(global.navigator, 'locks', {
445
- value: originalLocks,
446
- writable: true,
447
- })
448
- }
249
+ it('does not refresh when tab is not visible', async () => {
250
+ // Set visibility to hidden
251
+ Object.defineProperty(global, 'document', {
252
+ value: {
253
+ visibilityState: 'hidden',
254
+ addEventListener: vi.fn(),
255
+ removeEventListener: vi.fn(),
256
+ },
257
+ writable: true,
258
+ configurable: true,
259
+ })
260
+
261
+ const mockClient = {
262
+ observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
263
+ }
264
+ const mockClientFactory = vi.fn().mockReturnValue(mockClient)
265
+ const instance = createSanityInstance({
266
+ projectId: 'p',
267
+ dataset: 'd',
268
+ auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
449
269
  })
270
+ const initialState = authStore.getInitialState(instance, null)
271
+ initialState.authState = {
272
+ type: AuthStateType.LOGGED_IN,
273
+ token: 'sk-initial-token-st123',
274
+ currentUser: null,
275
+ }
276
+ const state = createStoreState(initialState)
277
+
278
+ const subscription = refreshStampedToken({state, instance, key: null})
279
+ subscriptions.push(subscription)
280
+
281
+ await vi.advanceTimersToNextTimerAsync()
282
+
283
+ // Verify that no refresh occurred
284
+ expect(mockClient.observable.request).not.toHaveBeenCalled()
285
+ const finalAuthState = state.get().authState
286
+ if (finalAuthState.type === AuthStateType.LOGGED_IN) {
287
+ expect(finalAuthState.token).toBe('sk-initial-token-st123')
288
+ }
450
289
  })
451
290
 
452
- // Restore other tests to their simpler form
453
291
  it('sets an error state when token refresh fails', async () => {
454
292
  const error = new Error('Refresh failed')
455
293
  const mockClient = {observable: {request: vi.fn(() => throwError(() => error))}}
456
294
  const mockClientFactory = vi.fn().mockReturnValue(mockClient)
457
295
  const instance = createSanityInstance({
296
+ projectId: 'p',
297
+ dataset: 'd',
458
298
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
459
299
  })
460
300
  const initialState = authStore.getInitialState(instance, null)
@@ -463,7 +303,6 @@ describe('refreshStampedToken', () => {
463
303
  token: 'sk-initial-token-st123',
464
304
  currentUser: null,
465
305
  }
466
- initialState.dashboardContext = {mode: 'test'}
467
306
  const state = createStoreState(initialState)
468
307
 
469
308
  const subscription = refreshStampedToken({state, instance, key: null})
@@ -482,6 +321,8 @@ describe('refreshStampedToken', () => {
482
321
  it('does nothing if user is not logged in', async () => {
483
322
  const mockClientFactory = vi.fn()
484
323
  const instance = createSanityInstance({
324
+ projectId: 'p',
325
+ dataset: 'd',
485
326
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
486
327
  })
487
328
  const initialState = authStore.getInitialState(instance, null)
@@ -497,8 +338,6 @@ describe('refreshStampedToken', () => {
497
338
  await vi.advanceTimersByTimeAsync(0)
498
339
 
499
340
  expect(mockClientFactory).not.toHaveBeenCalled()
500
- const locksRequest = navigator.locks.request as ReturnType<typeof vi.fn>
501
- expect(locksRequest).not.toHaveBeenCalled()
502
341
  expect(state.get().authState.type).toBe(AuthStateType.LOGGED_OUT)
503
342
  })
504
343
 
@@ -506,6 +345,8 @@ describe('refreshStampedToken', () => {
506
345
  const mockClient = {observable: {request: vi.fn()}}
507
346
  const mockClientFactory = vi.fn().mockReturnValue(mockClient)
508
347
  const instance = createSanityInstance({
348
+ projectId: 'p',
349
+ dataset: 'd',
509
350
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
510
351
  })
511
352
  const initialState = authStore.getInitialState(instance, null)
@@ -522,8 +363,6 @@ describe('refreshStampedToken', () => {
522
363
  await vi.advanceTimersByTimeAsync(0)
523
364
 
524
365
  expect(mockClient.observable.request).not.toHaveBeenCalled()
525
- const locksRequest = navigator.locks.request as ReturnType<typeof vi.fn>
526
- expect(locksRequest).not.toHaveBeenCalled()
527
366
  // Add type guard before accessing token property
528
367
  const finalAuthState = state.get().authState
529
368
  expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
@@ -532,92 +371,3 @@ describe('refreshStampedToken', () => {
532
371
  }
533
372
  })
534
373
  })
535
-
536
- describe('time-based refresh helpers', () => {
537
- let mockStorage: Storage
538
- const storageKey = 'my-test-key'
539
-
540
- beforeEach(() => {
541
- mockStorage = {
542
- getItem: vi.fn(),
543
- setItem: vi.fn(),
544
- removeItem: vi.fn(),
545
- clear: vi.fn(),
546
- key: vi.fn(),
547
- length: 0,
548
- }
549
- vi.useFakeTimers()
550
- })
551
-
552
- afterEach(() => {
553
- vi.useRealTimers()
554
- })
555
-
556
- describe('getLastRefreshTime', () => {
557
- it('returns 0 if storage is undefined', () => {
558
- expect(getLastRefreshTime(undefined, storageKey)).toBe(0)
559
- })
560
-
561
- it('returns 0 if item is not in storage', () => {
562
- vi.spyOn(mockStorage, 'getItem').mockReturnValue(null)
563
- expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
564
- expect(mockStorage.getItem).toHaveBeenCalledWith(`${storageKey}_last_refresh`)
565
- })
566
-
567
- it('returns the parsed timestamp from storage', () => {
568
- const now = Date.now()
569
- vi.spyOn(mockStorage, 'getItem').mockReturnValue(now.toString())
570
- expect(getLastRefreshTime(mockStorage, storageKey)).toBe(now)
571
- })
572
-
573
- it('returns 0 if stored data is malformed', () => {
574
- vi.spyOn(mockStorage, 'getItem').mockReturnValue('not a number')
575
- expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
576
- })
577
-
578
- it('returns 0 on storage access error', () => {
579
- vi.spyOn(mockStorage, 'getItem').mockImplementation(() => {
580
- throw new Error('Storage access failed')
581
- })
582
- expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
583
- })
584
- })
585
-
586
- describe('setLastRefreshTime', () => {
587
- it('sets the current timestamp in storage', () => {
588
- const now = Date.now()
589
- setLastRefreshTime(mockStorage, storageKey)
590
- expect(mockStorage.setItem).toHaveBeenCalledWith(`${storageKey}_last_refresh`, now.toString())
591
- })
592
-
593
- it('does not throw on storage access error', () => {
594
- vi.spyOn(mockStorage, 'setItem').mockImplementation(() => {
595
- throw new Error('Storage access failed')
596
- })
597
- expect(() => setLastRefreshTime(mockStorage, storageKey)).not.toThrow()
598
- })
599
- })
600
-
601
- describe('getNextRefreshDelay', () => {
602
- const REFRESH_INTERVAL = 12 * 60 * 60 * 1000
603
-
604
- it('returns 0 if last refresh time is not available', () => {
605
- vi.spyOn(mockStorage, 'getItem').mockReturnValue(null)
606
- expect(getNextRefreshDelay(mockStorage, storageKey)).toBe(0)
607
- })
608
-
609
- it('returns the remaining time until the next refresh', () => {
610
- const lastRefresh = Date.now() - 10000 // 10 seconds ago
611
- vi.spyOn(mockStorage, 'getItem').mockReturnValue(lastRefresh.toString())
612
-
613
- const delay = getNextRefreshDelay(mockStorage, storageKey)
614
- expect(delay).toBeCloseTo(REFRESH_INTERVAL - 10000, -2)
615
- })
616
-
617
- it('returns 0 if the refresh interval has passed', () => {
618
- const lastRefresh = Date.now() - REFRESH_INTERVAL - 5000 // 5 seconds past due
619
- vi.spyOn(mockStorage, 'getItem').mockReturnValue(lastRefresh.toString())
620
- expect(getNextRefreshDelay(mockStorage, storageKey)).toBe(0)
621
- })
622
- })
623
- })