@sanity/sdk 2.1.0 → 2.1.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.
- package/dist/index.d.ts +1705 -2163
- package/dist/index.js +497 -187
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/_exports/index.ts +22 -1
- package/src/auth/authStore.ts +1 -0
- package/src/auth/refreshStampedToken.test.ts +225 -6
- package/src/auth/refreshStampedToken.ts +95 -30
- package/src/presence/bifurTransport.test.ts +257 -0
- package/src/presence/bifurTransport.ts +108 -0
- package/src/presence/presenceStore.test.ts +247 -0
- package/src/presence/presenceStore.ts +163 -0
- package/src/presence/types.ts +70 -0
- package/src/query/queryStore.test.ts +4 -1
- package/src/releases/releasesStore.test.ts +5 -2
- package/src/users/reducers.ts +9 -3
- package/src/users/types.ts +17 -0
- package/src/users/usersConstants.ts +1 -0
- package/src/users/usersStore.test.ts +129 -9
- package/src/users/usersStore.ts +132 -4
- package/src/utils/defineIntent.test.ts +477 -0
- package/src/utils/defineIntent.ts +244 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"browserslist": "extends @sanity/browserslist-config",
|
|
43
43
|
"prettier": "@sanity/prettier-config",
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"@sanity/bifur-client": "^0.4.1",
|
|
45
46
|
"@sanity/client": "^7.2.1",
|
|
46
47
|
"@sanity/comlink": "^3.0.4",
|
|
47
48
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@sanity/browserslist-config": "^1.0.5",
|
|
61
|
-
"@sanity/pkg-utils": "^7.
|
|
62
|
+
"@sanity/pkg-utils": "^7.9.6",
|
|
62
63
|
"@sanity/prettier-config": "^1.0.3",
|
|
63
64
|
"@types/lodash-es": "^4.17.12",
|
|
64
65
|
"@vitest/coverage-v8": "3.1.2",
|
package/src/_exports/index.ts
CHANGED
|
@@ -104,6 +104,15 @@ export {type JsonMatch} from '../document/patchOperations'
|
|
|
104
104
|
export {type DocumentPermissionsResult, type PermissionDeniedReason} from '../document/permissions'
|
|
105
105
|
export type {FavoriteStatusResponse} from '../favorites/favorites'
|
|
106
106
|
export {getFavoritesState, resolveFavoritesState} from '../favorites/favorites'
|
|
107
|
+
export {getPresence} from '../presence/presenceStore'
|
|
108
|
+
export type {
|
|
109
|
+
DisconnectEvent,
|
|
110
|
+
PresenceLocation,
|
|
111
|
+
RollCallEvent,
|
|
112
|
+
StateEvent,
|
|
113
|
+
TransportEvent,
|
|
114
|
+
UserPresence,
|
|
115
|
+
} from '../presence/types'
|
|
107
116
|
export {getPreviewState, type GetPreviewStateOptions} from '../preview/getPreviewState'
|
|
108
117
|
export type {PreviewStoreState, PreviewValue, ValuePending} from '../preview/previewStore'
|
|
109
118
|
export {resolvePreview, type ResolvePreviewOptions} from '../preview/resolvePreview'
|
|
@@ -127,15 +136,27 @@ export {createSanityInstance, type SanityInstance} from '../store/createSanityIn
|
|
|
127
136
|
export {type Selector, type StateSource} from '../store/createStateSourceAction'
|
|
128
137
|
export {getUsersKey, parseUsersKey} from '../users/reducers'
|
|
129
138
|
export {
|
|
139
|
+
type GetUserOptions,
|
|
130
140
|
type GetUsersOptions,
|
|
131
141
|
type Membership,
|
|
142
|
+
type ResolveUserOptions,
|
|
132
143
|
type ResolveUsersOptions,
|
|
133
144
|
type SanityUser,
|
|
145
|
+
type SanityUserResponse,
|
|
134
146
|
type UserProfile,
|
|
147
|
+
type UsersGroupState,
|
|
148
|
+
type UsersStoreState,
|
|
135
149
|
} from '../users/types'
|
|
136
|
-
export {
|
|
150
|
+
export {
|
|
151
|
+
getUsersState,
|
|
152
|
+
getUserState,
|
|
153
|
+
loadMoreUsers,
|
|
154
|
+
resolveUser,
|
|
155
|
+
resolveUsers,
|
|
156
|
+
} from '../users/usersStore'
|
|
137
157
|
export {type FetcherStore, type FetcherStoreState} from '../utils/createFetcherStore'
|
|
138
158
|
export {createGroqSearchFilter} from '../utils/createGroqSearchFilter'
|
|
159
|
+
export {defineIntent, type Intent, type IntentFilter} from '../utils/defineIntent'
|
|
139
160
|
export {CORE_SDK_VERSION} from '../version'
|
|
140
161
|
export {
|
|
141
162
|
getIndexForKey,
|
package/src/auth/authStore.ts
CHANGED
|
@@ -8,7 +8,12 @@ import {createStoreState} from '../store/createStoreState'
|
|
|
8
8
|
import {AuthStateType} from './authStateType'
|
|
9
9
|
import {type AuthState, authStore} from './authStore'
|
|
10
10
|
// Import only the public function
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getLastRefreshTime,
|
|
13
|
+
getNextRefreshDelay,
|
|
14
|
+
refreshStampedToken,
|
|
15
|
+
setLastRefreshTime,
|
|
16
|
+
} from './refreshStampedToken'
|
|
12
17
|
|
|
13
18
|
// Type definitions for Web Locks (can be kept if needed for context)
|
|
14
19
|
// ... (Lock, LockOptions, LockGrantedCallback types)
|
|
@@ -16,6 +21,7 @@ import {refreshStampedToken} from './refreshStampedToken'
|
|
|
16
21
|
describe('refreshStampedToken', () => {
|
|
17
22
|
let mockStorage: Storage
|
|
18
23
|
let originalNavigator: typeof navigator // Restored
|
|
24
|
+
let originalDocument: Document
|
|
19
25
|
let subscriptions: Subscription[]
|
|
20
26
|
// mockLocksRequest removed
|
|
21
27
|
|
|
@@ -25,6 +31,19 @@ describe('refreshStampedToken', () => {
|
|
|
25
31
|
vi.useFakeTimers()
|
|
26
32
|
|
|
27
33
|
originalNavigator = global.navigator // Restore original navigator setup
|
|
34
|
+
|
|
35
|
+
// Mock document for visibility API
|
|
36
|
+
originalDocument = global.document
|
|
37
|
+
Object.defineProperty(global, 'document', {
|
|
38
|
+
value: {
|
|
39
|
+
visibilityState: 'visible',
|
|
40
|
+
addEventListener: vi.fn(),
|
|
41
|
+
removeEventListener: vi.fn(),
|
|
42
|
+
},
|
|
43
|
+
writable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
})
|
|
46
|
+
|
|
28
47
|
mockStorage = {
|
|
29
48
|
getItem: vi.fn(),
|
|
30
49
|
setItem: vi.fn(),
|
|
@@ -76,6 +95,11 @@ describe('refreshStampedToken', () => {
|
|
|
76
95
|
value: originalNavigator,
|
|
77
96
|
writable: true,
|
|
78
97
|
})
|
|
98
|
+
// Restore original document
|
|
99
|
+
Object.defineProperty(global, 'document', {
|
|
100
|
+
value: originalDocument,
|
|
101
|
+
writable: true,
|
|
102
|
+
})
|
|
79
103
|
// Restore real timers
|
|
80
104
|
try {
|
|
81
105
|
await vi.runAllTimersAsync() // Attempt to flush cleanly
|
|
@@ -122,6 +146,49 @@ describe('refreshStampedToken', () => {
|
|
|
122
146
|
const locksRequest = navigator.locks.request as ReturnType<typeof vi.fn>
|
|
123
147
|
expect(locksRequest).not.toHaveBeenCalled()
|
|
124
148
|
})
|
|
149
|
+
|
|
150
|
+
it('does not refresh when tab is not visible', async () => {
|
|
151
|
+
// Set visibility to hidden
|
|
152
|
+
Object.defineProperty(global, 'document', {
|
|
153
|
+
value: {
|
|
154
|
+
visibilityState: 'hidden',
|
|
155
|
+
addEventListener: vi.fn(),
|
|
156
|
+
removeEventListener: vi.fn(),
|
|
157
|
+
},
|
|
158
|
+
writable: true,
|
|
159
|
+
configurable: true,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const mockClient = {
|
|
163
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
164
|
+
}
|
|
165
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
166
|
+
const instance = createSanityInstance({
|
|
167
|
+
projectId: 'p',
|
|
168
|
+
dataset: 'd',
|
|
169
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
170
|
+
})
|
|
171
|
+
const initialState = authStore.getInitialState(instance)
|
|
172
|
+
initialState.authState = {
|
|
173
|
+
type: AuthStateType.LOGGED_IN,
|
|
174
|
+
token: 'sk-initial-token-st123',
|
|
175
|
+
currentUser: null,
|
|
176
|
+
}
|
|
177
|
+
initialState.dashboardContext = {mode: 'test'}
|
|
178
|
+
const state = createStoreState(initialState)
|
|
179
|
+
|
|
180
|
+
const subscription = refreshStampedToken({state, instance})
|
|
181
|
+
subscriptions.push(subscription)
|
|
182
|
+
|
|
183
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
184
|
+
|
|
185
|
+
// Verify that no refresh occurred
|
|
186
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
187
|
+
const finalAuthState = state.get().authState
|
|
188
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
189
|
+
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
190
|
+
}
|
|
191
|
+
})
|
|
125
192
|
})
|
|
126
193
|
|
|
127
194
|
describe('non-dashboard context', () => {
|
|
@@ -150,11 +217,19 @@ describe('refreshStampedToken', () => {
|
|
|
150
217
|
subscriptions.push(subscription!)
|
|
151
218
|
}).not.toThrow()
|
|
152
219
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
220
|
+
// DO NOT advance timers or yield here - focus on immediate observable logic
|
|
221
|
+
// We cannot reliably test that failingLocksRequest is called due to async/timer issues,
|
|
222
|
+
// but we *can* test the consequence of it resolving to false.
|
|
223
|
+
|
|
224
|
+
// VERIFY THE OUTCOME:
|
|
225
|
+
// Check client request was NOT made (because filter(hasLock => hasLock) receives false)
|
|
226
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
227
|
+
// Check state remains unchanged
|
|
228
|
+
const finalAuthState = state.get().authState
|
|
229
|
+
expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
|
|
230
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
231
|
+
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
232
|
+
}
|
|
158
233
|
})
|
|
159
234
|
|
|
160
235
|
it('skips refresh if lock request returns false', async () => {
|
|
@@ -209,6 +284,61 @@ describe('refreshStampedToken', () => {
|
|
|
209
284
|
})
|
|
210
285
|
})
|
|
211
286
|
|
|
287
|
+
describe('unsupported environments', () => {
|
|
288
|
+
it('falls back to immediate refresh if Web Locks API is not supported', async () => {
|
|
289
|
+
// Temporarily remove navigator.locks for this test
|
|
290
|
+
const originalLocks = navigator.locks
|
|
291
|
+
Object.defineProperty(global.navigator, 'locks', {
|
|
292
|
+
value: undefined,
|
|
293
|
+
writable: true,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const mockClient = {
|
|
298
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-immediately-st123'}))},
|
|
299
|
+
}
|
|
300
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
301
|
+
const instance = createSanityInstance({
|
|
302
|
+
projectId: 'p',
|
|
303
|
+
dataset: 'd',
|
|
304
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
305
|
+
})
|
|
306
|
+
const initialState = authStore.getInitialState(instance)
|
|
307
|
+
initialState.authState = {
|
|
308
|
+
type: AuthStateType.LOGGED_IN,
|
|
309
|
+
token: 'sk-initial-token-st123',
|
|
310
|
+
currentUser: null,
|
|
311
|
+
}
|
|
312
|
+
const state = createStoreState(initialState)
|
|
313
|
+
|
|
314
|
+
const subscription = refreshStampedToken({state, instance})
|
|
315
|
+
subscriptions.push(subscription)
|
|
316
|
+
|
|
317
|
+
// Advance timers to allow the async `performRefresh` to execute
|
|
318
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
319
|
+
|
|
320
|
+
// Verify the refresh was performed and state was updated
|
|
321
|
+
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
322
|
+
const finalAuthState = state.get().authState
|
|
323
|
+
expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
|
|
324
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
325
|
+
expect(finalAuthState.token).toBe('sk-refreshed-immediately-st123')
|
|
326
|
+
}
|
|
327
|
+
// Verify token was set in storage
|
|
328
|
+
expect(mockStorage.setItem).toHaveBeenCalledWith(
|
|
329
|
+
initialState.options.storageKey,
|
|
330
|
+
JSON.stringify({token: 'sk-refreshed-immediately-st123'}),
|
|
331
|
+
)
|
|
332
|
+
} finally {
|
|
333
|
+
// Restore navigator.locks
|
|
334
|
+
Object.defineProperty(global.navigator, 'locks', {
|
|
335
|
+
value: originalLocks,
|
|
336
|
+
writable: true,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
212
342
|
// Restore other tests to their simpler form
|
|
213
343
|
it('sets an error state when token refresh fails', async () => {
|
|
214
344
|
const error = new Error('Refresh failed')
|
|
@@ -298,3 +428,92 @@ describe('refreshStampedToken', () => {
|
|
|
298
428
|
}
|
|
299
429
|
})
|
|
300
430
|
})
|
|
431
|
+
|
|
432
|
+
describe('time-based refresh helpers', () => {
|
|
433
|
+
let mockStorage: Storage
|
|
434
|
+
const storageKey = 'my-test-key'
|
|
435
|
+
|
|
436
|
+
beforeEach(() => {
|
|
437
|
+
mockStorage = {
|
|
438
|
+
getItem: vi.fn(),
|
|
439
|
+
setItem: vi.fn(),
|
|
440
|
+
removeItem: vi.fn(),
|
|
441
|
+
clear: vi.fn(),
|
|
442
|
+
key: vi.fn(),
|
|
443
|
+
length: 0,
|
|
444
|
+
}
|
|
445
|
+
vi.useFakeTimers()
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
afterEach(() => {
|
|
449
|
+
vi.useRealTimers()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
describe('getLastRefreshTime', () => {
|
|
453
|
+
it('returns 0 if storage is undefined', () => {
|
|
454
|
+
expect(getLastRefreshTime(undefined, storageKey)).toBe(0)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('returns 0 if item is not in storage', () => {
|
|
458
|
+
vi.spyOn(mockStorage, 'getItem').mockReturnValue(null)
|
|
459
|
+
expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
|
|
460
|
+
expect(mockStorage.getItem).toHaveBeenCalledWith(`${storageKey}_last_refresh`)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('returns the parsed timestamp from storage', () => {
|
|
464
|
+
const now = Date.now()
|
|
465
|
+
vi.spyOn(mockStorage, 'getItem').mockReturnValue(now.toString())
|
|
466
|
+
expect(getLastRefreshTime(mockStorage, storageKey)).toBe(now)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('returns 0 if stored data is malformed', () => {
|
|
470
|
+
vi.spyOn(mockStorage, 'getItem').mockReturnValue('not a number')
|
|
471
|
+
expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('returns 0 on storage access error', () => {
|
|
475
|
+
vi.spyOn(mockStorage, 'getItem').mockImplementation(() => {
|
|
476
|
+
throw new Error('Storage access failed')
|
|
477
|
+
})
|
|
478
|
+
expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
describe('setLastRefreshTime', () => {
|
|
483
|
+
it('sets the current timestamp in storage', () => {
|
|
484
|
+
const now = Date.now()
|
|
485
|
+
setLastRefreshTime(mockStorage, storageKey)
|
|
486
|
+
expect(mockStorage.setItem).toHaveBeenCalledWith(`${storageKey}_last_refresh`, now.toString())
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('does not throw on storage access error', () => {
|
|
490
|
+
vi.spyOn(mockStorage, 'setItem').mockImplementation(() => {
|
|
491
|
+
throw new Error('Storage access failed')
|
|
492
|
+
})
|
|
493
|
+
expect(() => setLastRefreshTime(mockStorage, storageKey)).not.toThrow()
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
describe('getNextRefreshDelay', () => {
|
|
498
|
+
const REFRESH_INTERVAL = 12 * 60 * 60 * 1000
|
|
499
|
+
|
|
500
|
+
it('returns 0 if last refresh time is not available', () => {
|
|
501
|
+
vi.spyOn(mockStorage, 'getItem').mockReturnValue(null)
|
|
502
|
+
expect(getNextRefreshDelay(mockStorage, storageKey)).toBe(0)
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('returns the remaining time until the next refresh', () => {
|
|
506
|
+
const lastRefresh = Date.now() - 10000 // 10 seconds ago
|
|
507
|
+
vi.spyOn(mockStorage, 'getItem').mockReturnValue(lastRefresh.toString())
|
|
508
|
+
|
|
509
|
+
const delay = getNextRefreshDelay(mockStorage, storageKey)
|
|
510
|
+
expect(delay).toBeCloseTo(REFRESH_INTERVAL - 10000, -2)
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('returns 0 if the refresh interval has passed', () => {
|
|
514
|
+
const lastRefresh = Date.now() - REFRESH_INTERVAL - 5000 // 5 seconds past due
|
|
515
|
+
vi.spyOn(mockStorage, 'getItem').mockReturnValue(lastRefresh.toString())
|
|
516
|
+
expect(getNextRefreshDelay(mockStorage, storageKey)).toBe(0)
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
})
|
|
@@ -20,16 +20,19 @@ import {type AuthState, type AuthStoreState} from './authStore'
|
|
|
20
20
|
const REFRESH_INTERVAL = 12 * 60 * 60 * 1000 // 12 hours in milliseconds
|
|
21
21
|
const LOCK_NAME = 'sanity-token-refresh-lock'
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/** @internal */
|
|
24
|
+
export function getLastRefreshTime(storageArea: Storage | undefined, storageKey: string): number {
|
|
24
25
|
try {
|
|
25
26
|
const data = storageArea?.getItem(`${storageKey}_last_refresh`)
|
|
26
|
-
|
|
27
|
+
const parsed = data ? parseInt(data, 10) : 0
|
|
28
|
+
return isNaN(parsed) ? 0 : parsed
|
|
27
29
|
} catch {
|
|
28
30
|
return 0
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
/** @internal */
|
|
35
|
+
export function setLastRefreshTime(storageArea: Storage | undefined, storageKey: string): void {
|
|
33
36
|
try {
|
|
34
37
|
storageArea?.setItem(`${storageKey}_last_refresh`, Date.now().toString())
|
|
35
38
|
} catch {
|
|
@@ -37,7 +40,8 @@ function setLastRefreshTime(storageArea: Storage | undefined, storageKey: string
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
/** @internal */
|
|
44
|
+
export function getNextRefreshDelay(storageArea: Storage | undefined, storageKey: string): number {
|
|
41
45
|
const lastRefresh = getLastRefreshTime(storageArea, storageKey)
|
|
42
46
|
if (!lastRefresh) return 0
|
|
43
47
|
|
|
@@ -127,6 +131,12 @@ async function acquireTokenRefreshLock(
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
function shouldRefreshToken(lastRefresh: number | undefined): boolean {
|
|
135
|
+
if (!lastRefresh) return true
|
|
136
|
+
const timeSinceLastRefresh = Date.now() - lastRefresh
|
|
137
|
+
return timeSinceLastRefresh >= REFRESH_INTERVAL
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
/**
|
|
131
141
|
* @internal
|
|
132
142
|
*/
|
|
@@ -178,51 +188,106 @@ export const refreshStampedToken = ({state}: StoreContext<AuthStoreState>): Subs
|
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
if (storeState.dashboardContext) {
|
|
181
|
-
return
|
|
182
|
-
|
|
191
|
+
return new Observable<{token: string}>((subscriber) => {
|
|
192
|
+
const visibilityHandler = () => {
|
|
193
|
+
const currentState = state.get()
|
|
194
|
+
if (
|
|
195
|
+
document.visibilityState === 'visible' &&
|
|
196
|
+
currentState.authState.type === AuthStateType.LOGGED_IN &&
|
|
197
|
+
shouldRefreshToken(currentState.authState.lastTokenRefresh)
|
|
198
|
+
) {
|
|
199
|
+
createTokenRefreshStream(
|
|
200
|
+
currentState.authState.token,
|
|
201
|
+
clientFactory,
|
|
202
|
+
apiHost,
|
|
203
|
+
).subscribe({
|
|
204
|
+
next: (response) => {
|
|
205
|
+
state.set('setRefreshStampedToken', (prev) => ({
|
|
206
|
+
authState:
|
|
207
|
+
prev.authState.type === AuthStateType.LOGGED_IN
|
|
208
|
+
? {
|
|
209
|
+
...prev.authState,
|
|
210
|
+
token: response.token,
|
|
211
|
+
lastTokenRefresh: Date.now(),
|
|
212
|
+
}
|
|
213
|
+
: prev.authState,
|
|
214
|
+
}))
|
|
215
|
+
subscriber.next(response)
|
|
216
|
+
},
|
|
217
|
+
error: (error) => subscriber.error(error),
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const timerSubscription = timer(REFRESH_INTERVAL, REFRESH_INTERVAL)
|
|
223
|
+
.pipe(
|
|
224
|
+
filter(() => document.visibilityState === 'visible'),
|
|
225
|
+
switchMap(() => {
|
|
226
|
+
const currentState = state.get().authState
|
|
227
|
+
if (currentState.type !== AuthStateType.LOGGED_IN) {
|
|
228
|
+
throw new Error('User logged out before refresh could complete')
|
|
229
|
+
}
|
|
230
|
+
return createTokenRefreshStream(currentState.token, clientFactory, apiHost)
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
.subscribe({
|
|
234
|
+
next: (response) => {
|
|
235
|
+
state.set('setRefreshStampedToken', (prev) => ({
|
|
236
|
+
authState:
|
|
237
|
+
prev.authState.type === AuthStateType.LOGGED_IN
|
|
238
|
+
? {
|
|
239
|
+
...prev.authState,
|
|
240
|
+
token: response.token,
|
|
241
|
+
lastTokenRefresh: Date.now(),
|
|
242
|
+
}
|
|
243
|
+
: prev.authState,
|
|
244
|
+
}))
|
|
245
|
+
subscriber.next(response)
|
|
246
|
+
},
|
|
247
|
+
error: (error) => subscriber.error(error),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
document.addEventListener('visibilitychange', visibilityHandler)
|
|
251
|
+
|
|
252
|
+
return () => {
|
|
253
|
+
document.removeEventListener('visibilitychange', visibilityHandler)
|
|
254
|
+
timerSubscription.unsubscribe()
|
|
255
|
+
}
|
|
256
|
+
}).pipe(
|
|
183
257
|
takeWhile(() => state.get().authState.type === AuthStateType.LOGGED_IN),
|
|
184
|
-
|
|
185
|
-
switchMap(() =>
|
|
186
|
-
createTokenRefreshStream(storeState.authState.token, clientFactory, apiHost),
|
|
187
|
-
),
|
|
188
|
-
// Map the successful response for the outer subscribe block
|
|
189
|
-
map((response) => ({token: response.token})),
|
|
258
|
+
map((response: {token: string}) => ({token: response.token})),
|
|
190
259
|
)
|
|
191
260
|
}
|
|
192
261
|
|
|
193
|
-
// If not in dashboard context, use lock-based refresh
|
|
194
|
-
// exhaustMap prevents this from running again if state changes while lock logic is active.
|
|
195
|
-
// NOTE: Based on Web Locks API, this `from(acquireTokenRefreshLock(...))` observable
|
|
196
|
-
// will likely NOT emit if the lock is successfully acquired, as the underlying promise
|
|
197
|
-
// may never resolve due to the while(true) loop. This path needs verification.
|
|
262
|
+
// If not in dashboard context, use lock-based refresh
|
|
198
263
|
return from(acquireTokenRefreshLock(performRefresh, storageArea, storageKey)).pipe(
|
|
199
264
|
filter((hasLock) => hasLock),
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
265
|
+
map(() => {
|
|
266
|
+
const currentState = state.get().authState
|
|
267
|
+
if (currentState.type !== AuthStateType.LOGGED_IN) {
|
|
268
|
+
throw new Error('User logged out before refresh could complete')
|
|
269
|
+
}
|
|
270
|
+
return {token: currentState.token} as const
|
|
271
|
+
}),
|
|
203
272
|
)
|
|
204
273
|
}),
|
|
205
274
|
)
|
|
206
275
|
|
|
207
276
|
return refreshToken$.subscribe({
|
|
208
|
-
next: (response) => {
|
|
209
|
-
// This block now primarily handles updates from the dashboard timer path,
|
|
210
|
-
// or potentially from the lock path if acquireTokenRefreshLock resolves unexpectedly.
|
|
211
|
-
// exhaustMap prevents this state update from causing an immediate loop.
|
|
277
|
+
next: (response: {token: string}) => {
|
|
212
278
|
state.set('setRefreshStampedToken', (prev) => ({
|
|
213
279
|
authState:
|
|
214
280
|
prev.authState.type === AuthStateType.LOGGED_IN
|
|
215
|
-
? {
|
|
281
|
+
? {
|
|
282
|
+
...prev.authState,
|
|
283
|
+
token: response.token,
|
|
284
|
+
lastTokenRefresh: Date.now(),
|
|
285
|
+
}
|
|
216
286
|
: prev.authState,
|
|
217
287
|
}))
|
|
218
288
|
storageArea?.setItem(storageKey, JSON.stringify({token: response.token}))
|
|
219
289
|
},
|
|
220
290
|
error: (error) => {
|
|
221
|
-
// Log errors from either refresh path
|
|
222
|
-
// Consider how refresh failures should affect the overall auth state
|
|
223
|
-
// E.g., maybe attempt retry, or eventually set state to ERROR if retries fail
|
|
224
|
-
// For now, just log, avoiding immediate state change to ERROR
|
|
225
|
-
// console.error('Token refresh failed:', error) // Removed to satisfy linter
|
|
226
291
|
state.set('setRefreshStampedTokenError', {authState: {type: AuthStateType.ERROR, error}})
|
|
227
292
|
},
|
|
228
293
|
})
|