@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.
@@ -0,0 +1,163 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {createSelector} from 'reselect'
3
+ import {combineLatest, distinctUntilChanged, filter, map, of, Subscription, switchMap} from 'rxjs'
4
+
5
+ import {getTokenState} from '../auth/authStore'
6
+ import {getClient} from '../client/clientStore'
7
+ import {bindActionByDataset} from '../store/createActionBinder'
8
+ import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
9
+ import {defineStore, type StoreContext} from '../store/defineStore'
10
+ import {type SanityUser} from '../users/types'
11
+ import {getUserState} from '../users/usersStore'
12
+ import {createBifurTransport} from './bifurTransport'
13
+ import {type PresenceLocation, type TransportEvent, type UserPresence} from './types'
14
+
15
+ type PresenceStoreState = {
16
+ locations: Map<string, {userId: string; locations: PresenceLocation[]}>
17
+ users: Record<string, SanityUser | undefined>
18
+ }
19
+
20
+ const getInitialState = (): PresenceStoreState => ({
21
+ locations: new Map<string, {userId: string; locations: PresenceLocation[]}>(),
22
+ users: {},
23
+ })
24
+
25
+ /** @public */
26
+ export const presenceStore = defineStore<PresenceStoreState>({
27
+ name: 'presence',
28
+ getInitialState,
29
+ initialize: (context: StoreContext<PresenceStoreState>) => {
30
+ const {instance, state} = context
31
+ const sessionId = crypto.randomUUID()
32
+
33
+ const client = getClient(instance, {
34
+ apiVersion: '2022-06-30',
35
+ })
36
+
37
+ const token$ = getTokenState(instance).observable.pipe(distinctUntilChanged())
38
+
39
+ const [incomingEvents$, dispatch] = createBifurTransport({
40
+ client: client as SanityClient,
41
+ token$,
42
+ sessionId,
43
+ })
44
+
45
+ const subscription = new Subscription()
46
+
47
+ subscription.add(
48
+ incomingEvents$.subscribe((event: TransportEvent) => {
49
+ if ('sessionId' in event && event.sessionId === sessionId) {
50
+ return
51
+ }
52
+
53
+ if (event.type === 'state') {
54
+ state.set('presence/state', (prevState: PresenceStoreState) => {
55
+ const newLocations = new Map(prevState.locations)
56
+ newLocations.set(event.sessionId, {
57
+ userId: event.userId,
58
+ locations: event.locations,
59
+ })
60
+
61
+ return {
62
+ ...prevState,
63
+ locations: newLocations,
64
+ }
65
+ })
66
+ } else if (event.type === 'disconnect') {
67
+ state.set('presence/disconnect', (prevState: PresenceStoreState) => {
68
+ const newLocations = new Map(prevState.locations)
69
+ newLocations.delete(event.sessionId)
70
+ return {...prevState, locations: newLocations}
71
+ })
72
+ }
73
+ }),
74
+ )
75
+
76
+ dispatch({type: 'rollCall'}).subscribe()
77
+
78
+ return () => {
79
+ dispatch({type: 'disconnect'}).subscribe()
80
+ subscription.unsubscribe()
81
+ }
82
+ },
83
+ })
84
+
85
+ const selectLocations = (state: PresenceStoreState) => state.locations
86
+ const selectUsers = (state: PresenceStoreState) => state.users
87
+
88
+ const selectPresence = createSelector(
89
+ selectLocations,
90
+ selectUsers,
91
+ (locations, users): UserPresence[] => {
92
+ return Array.from(locations.entries()).map(([sessionId, {userId, locations: locs}]) => {
93
+ const user = users[userId]
94
+
95
+ return {
96
+ user:
97
+ user ||
98
+ ({
99
+ id: userId,
100
+ displayName: 'Unknown user',
101
+ name: 'Unknown user',
102
+ email: '',
103
+ } as unknown as SanityUser),
104
+ sessionId,
105
+ locations: locs,
106
+ }
107
+ })
108
+ },
109
+ )
110
+
111
+ /** @public */
112
+ export const getPresence = bindActionByDataset(
113
+ presenceStore,
114
+ createStateSourceAction({
115
+ selector: (context: SelectorContext<PresenceStoreState>): UserPresence[] =>
116
+ selectPresence(context.state),
117
+ onSubscribe: (context) => {
118
+ const userIds$ = context.state.observable.pipe(
119
+ map((state) =>
120
+ Array.from(state.locations.values())
121
+ .map((l) => l.userId)
122
+ .filter((id): id is string => !!id),
123
+ ),
124
+ distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])),
125
+ )
126
+
127
+ const subscription = userIds$
128
+ .pipe(
129
+ switchMap((userIds) => {
130
+ if (userIds.length === 0) {
131
+ return of([])
132
+ }
133
+ const userObservables = userIds.map((userId) =>
134
+ getUserState(context.instance, {
135
+ userId,
136
+ resourceType: 'project',
137
+ projectId: context.instance.config.projectId,
138
+ }).pipe(filter((v): v is NonNullable<typeof v> => !!v)),
139
+ )
140
+ return combineLatest(userObservables)
141
+ }),
142
+ )
143
+ .subscribe((users) => {
144
+ if (!users) {
145
+ return
146
+ }
147
+ context.state.set('presence/users', (prevState) => ({
148
+ ...prevState,
149
+ users: {
150
+ ...prevState.users,
151
+ ...users.reduce<Record<string, SanityUser>>((acc, user) => {
152
+ if (user) {
153
+ acc[user.profile.id] = user
154
+ }
155
+ return acc
156
+ }, {}),
157
+ },
158
+ }))
159
+ })
160
+ return () => subscription.unsubscribe()
161
+ },
162
+ }),
163
+ )
@@ -0,0 +1,70 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {type Observable} from 'rxjs'
3
+
4
+ import {type SanityUser} from '../users/types'
5
+
6
+ /** @public */
7
+ export interface PresenceLocation {
8
+ type: 'document'
9
+ documentId: string
10
+ path: string[]
11
+ lastActiveAt: string
12
+ }
13
+
14
+ /** @public */
15
+ export interface UserPresence {
16
+ user: SanityUser
17
+ locations: PresenceLocation[]
18
+ sessionId: string
19
+ }
20
+
21
+ /** @public */
22
+ export type PresenceTransport = [
23
+ incomingEvents$: Observable<TransportEvent>,
24
+ dispatchMessage: (message: TransportMessage) => Observable<void>,
25
+ ]
26
+
27
+ /** @public */
28
+ export type TransportEvent = RollCallEvent | StateEvent | DisconnectEvent
29
+
30
+ /** @public */
31
+ export interface RollCallEvent {
32
+ type: 'rollCall'
33
+ userId: string
34
+ sessionId: string
35
+ }
36
+
37
+ /** @public */
38
+ export interface StateEvent {
39
+ type: 'state'
40
+ userId: string
41
+ sessionId: string
42
+ timestamp: string
43
+ locations: PresenceLocation[]
44
+ }
45
+
46
+ /** @public */
47
+ export interface DisconnectEvent {
48
+ type: 'disconnect'
49
+ userId: string
50
+ sessionId: string
51
+ timestamp: string
52
+ }
53
+
54
+ /** @public */
55
+ export type TransportMessage =
56
+ | {type: 'rollCall'}
57
+ | {type: 'state'; locations: PresenceLocation[]}
58
+ | {type: 'disconnect'}
59
+
60
+ /** @public */
61
+ export interface BifurTransportOptions {
62
+ client: SanityClient
63
+ token$: Observable<string | null>
64
+ sessionId: string
65
+ }
66
+
67
+ /** @public */
68
+ export interface PresenceStore {
69
+ locations$: Observable<PresenceLocation[]>
70
+ }
@@ -20,6 +20,7 @@ describe('queryStore', () => {
20
20
  let instance: SanityInstance
21
21
  let liveEvents: Subject<LiveEvent>
22
22
  let fetch: SanityClient['observable']['fetch']
23
+ let listen: SanityClient['observable']['listen']
23
24
  // Mock data for testing
24
25
  const mockData = {
25
26
  movies: [
@@ -37,6 +38,8 @@ describe('queryStore', () => {
37
38
  of({result: mockData.movies, syncTags: []}).pipe(delay(0)),
38
39
  ) as SanityClient['observable']['fetch']
39
40
 
41
+ listen = vi.fn().mockReturnValue(of(mockData.movies))
42
+
40
43
  liveEvents = new Subject<LiveEvent>()
41
44
 
42
45
  const events = vi.fn().mockReturnValue(liveEvents) as SanityClient['live']['events']
@@ -47,7 +50,7 @@ describe('queryStore', () => {
47
50
  observable: of({
48
51
  config,
49
52
  live: {events},
50
- observable: {fetch},
53
+ observable: {fetch, listen},
51
54
  } as SanityClient),
52
55
  } as StateSource<SanityClient>)
53
56
  })
@@ -17,7 +17,7 @@ vi.mock('../utils/listenQuery', () => ({
17
17
  }))
18
18
 
19
19
  // Mock console.error to prevent test runner noise and allow verification
20
- const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
20
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>
21
21
 
22
22
  describe('releasesStore', () => {
23
23
  let instance: SanityInstance
@@ -25,7 +25,7 @@ describe('releasesStore', () => {
25
25
 
26
26
  beforeEach(() => {
27
27
  vi.resetAllMocks()
28
- consoleErrorSpy.mockClear()
28
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
29
29
 
30
30
  instance = createSanityInstance({projectId: 'test', dataset: 'test'})
31
31
 
@@ -36,6 +36,7 @@ describe('releasesStore', () => {
36
36
 
37
37
  afterEach(() => {
38
38
  instance.dispose()
39
+ consoleErrorSpy.mockRestore()
39
40
  })
40
41
 
41
42
  it('should set active releases state when listenQuery succeeds', async () => {
@@ -63,6 +64,8 @@ describe('releasesStore', () => {
63
64
 
64
65
  expect(state.getCurrent()).toEqual(mockReleases.reverse())
65
66
  expect(consoleErrorSpy).not.toHaveBeenCalled()
67
+
68
+ vi.useRealTimers()
66
69
  })
67
70
 
68
71
  it('should update active releases state when listenQuery emits new data', async () => {
@@ -12,11 +12,16 @@ export const getUsersKey = (
12
12
  organizationId,
13
13
  batchSize = DEFAULT_USERS_BATCH_SIZE,
14
14
  projectId = instance.config.projectId,
15
+ userId,
15
16
  }: GetUsersOptions = {},
16
17
  ): string =>
17
- JSON.stringify({resourceType, organizationId, batchSize, projectId} satisfies ReturnType<
18
- typeof parseUsersKey
19
- >)
18
+ JSON.stringify({
19
+ resourceType,
20
+ organizationId,
21
+ batchSize,
22
+ projectId,
23
+ userId,
24
+ } satisfies ReturnType<typeof parseUsersKey>)
20
25
 
21
26
  /** @internal */
22
27
  export const parseUsersKey = (
@@ -26,6 +31,7 @@ export const parseUsersKey = (
26
31
  resourceType?: 'organization' | 'project'
27
32
  projectId?: string
28
33
  organizationId?: string
34
+ userId?: string
29
35
  } => JSON.parse(key)
30
36
 
31
37
  export const addSubscription =
@@ -46,6 +46,7 @@ export interface GetUsersOptions extends ProjectHandle {
46
46
  resourceType?: 'organization' | 'project'
47
47
  batchSize?: number
48
48
  organizationId?: string
49
+ userId?: string
49
50
  }
50
51
 
51
52
  /**
@@ -83,3 +84,19 @@ export interface UsersStoreState {
83
84
  export interface ResolveUsersOptions extends GetUsersOptions {
84
85
  signal?: AbortSignal
85
86
  }
87
+
88
+ /**
89
+ * @public
90
+ */
91
+ export interface GetUserOptions extends ProjectHandle {
92
+ userId: string
93
+ resourceType?: 'organization' | 'project'
94
+ organizationId?: string
95
+ }
96
+
97
+ /**
98
+ * @public
99
+ */
100
+ export interface ResolveUserOptions extends GetUserOptions {
101
+ signal?: AbortSignal
102
+ }
@@ -1,4 +1,5 @@
1
1
  // NOTE: currently this API is only available on vX
2
2
  export const API_VERSION = 'vX'
3
+ export const PROJECT_API_VERSION = '2025-07-18'
3
4
  export const USERS_STATE_CLEAR_DELAY = 5000
4
5
  export const DEFAULT_USERS_BATCH_SIZE = 100
@@ -2,11 +2,18 @@ import {type SanityClient} from '@sanity/client'
2
2
  import {delay, filter, firstValueFrom, Observable, of} from 'rxjs'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
- import {getClientState} from '../client/clientStore'
5
+ import {getClient, getClientState} from '../client/clientStore'
6
6
  import {createSanityInstance} from '../store/createSanityInstance'
7
7
  import {type StateSource} from '../store/createStateSourceAction'
8
8
  import {type GetUsersOptions, type SanityUser, type SanityUserResponse} from './types'
9
- import {getUsersState, loadMoreUsers, resolveUsers} from './usersStore'
9
+ import {
10
+ getUsersState,
11
+ getUserState,
12
+ loadMoreUsers,
13
+ type PatchedSanityUserFromClient,
14
+ resolveUser,
15
+ resolveUsers,
16
+ } from './usersStore'
10
17
 
11
18
  vi.mock('./usersConstants', async (importOriginal) => ({
12
19
  ...(await importOriginal<typeof import('./usersConstants')>()),
@@ -64,13 +71,19 @@ describe('usersStore', () => {
64
71
  beforeEach(() => {
65
72
  request = vi.fn().mockReturnValue(of(mockResponse).pipe(delay(0)))
66
73
 
67
- vi.mocked(getClientState).mockReturnValue({
68
- observable: of({
69
- observable: {
70
- request,
71
- },
72
- } as SanityClient),
73
- } as StateSource<SanityClient>)
74
+ vi.mocked(getClientState).mockImplementation(() => {
75
+ const client = {
76
+ observable: {request},
77
+ } as unknown as SanityClient
78
+ return {
79
+ observable: of(client),
80
+ } as StateSource<SanityClient>
81
+ })
82
+ vi.mocked(getClient).mockReturnValue({
83
+ observable: {
84
+ request,
85
+ },
86
+ } as unknown as SanityClient)
74
87
  })
75
88
 
76
89
  it('initializes users state and cleans up after unsubscribe', async () => {
@@ -391,4 +404,111 @@ describe('usersStore', () => {
391
404
  unsubscribe2()
392
405
  instance.dispose()
393
406
  })
407
+
408
+ describe('getUserState', () => {
409
+ beforeEach(() => {
410
+ // Clear all mocks between tests
411
+ vi.clearAllMocks()
412
+ })
413
+
414
+ it('fetches a single user with a project-scoped ID', async () => {
415
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
416
+ const projectUserId = 'p12345'
417
+ const mockProjectUser: PatchedSanityUserFromClient = {
418
+ id: projectUserId,
419
+ sanityUserId: projectUserId,
420
+ displayName: 'Project User',
421
+ createdAt: '2023-01-01T00:00:00Z',
422
+ updatedAt: '2023-01-01T00:00:00Z',
423
+ isCurrentUser: false,
424
+ projectId: 'project1',
425
+ familyName: null,
426
+ givenName: null,
427
+ middleName: null,
428
+ imageUrl: null,
429
+ email: 'project@example.com',
430
+ provider: 'google',
431
+ }
432
+
433
+ const specificRequest = vi.fn().mockReturnValue(of(mockProjectUser).pipe(delay(0)))
434
+ vi.mocked(getClient).mockReturnValue({
435
+ observable: {
436
+ request: specificRequest,
437
+ },
438
+ } as unknown as SanityClient)
439
+
440
+ const user$ = getUserState(instance, {userId: projectUserId, projectId: 'project1'})
441
+
442
+ const result = await firstValueFrom(user$.pipe(filter((i) => i !== undefined)))
443
+
444
+ expect(getClient).toHaveBeenCalledWith(
445
+ instance,
446
+ expect.objectContaining({
447
+ projectId: 'project1',
448
+ useProjectHostname: true,
449
+ }),
450
+ )
451
+ expect(specificRequest).toHaveBeenCalledWith({
452
+ method: 'GET',
453
+ uri: `/users/${projectUserId}`,
454
+ })
455
+
456
+ const expectedUser: SanityUser = {
457
+ sanityUserId: projectUserId,
458
+ profile: {
459
+ id: projectUserId,
460
+ displayName: 'Project User',
461
+ familyName: undefined,
462
+ givenName: undefined,
463
+ middleName: undefined,
464
+ imageUrl: undefined,
465
+ createdAt: '2023-01-01T00:00:00Z',
466
+ updatedAt: '2023-01-01T00:00:00Z',
467
+ isCurrentUser: false,
468
+ email: 'project@example.com',
469
+ provider: 'google',
470
+ },
471
+ memberships: [],
472
+ }
473
+ expect(result).toEqual(expectedUser)
474
+
475
+ instance.dispose()
476
+ })
477
+
478
+ it('fetches a single user with a global-scoped ID', async () => {
479
+ const instance = createSanityInstance({
480
+ projectId: 'test',
481
+ dataset: 'test',
482
+ })
483
+ const globalUserId = 'g12345'
484
+ const mockGlobalUser: SanityUser = {
485
+ sanityUserId: globalUserId,
486
+ profile: {
487
+ id: 'profile-g1',
488
+ displayName: 'Global User',
489
+ email: 'global@example.com',
490
+ provider: 'google',
491
+ createdAt: '2023-01-01T00:00:00Z',
492
+ },
493
+ memberships: [],
494
+ }
495
+ const mockGlobalUserResponse: SanityUserResponse = {
496
+ data: [mockGlobalUser],
497
+ totalCount: 1,
498
+ nextCursor: null,
499
+ }
500
+
501
+ // Mock the request to return the global user response
502
+ vi.mocked(request).mockReturnValue(of(mockGlobalUserResponse))
503
+
504
+ const result = await resolveUser(instance, {
505
+ userId: globalUserId,
506
+ projectId: 'project1',
507
+ })
508
+
509
+ expect(result).toEqual(mockGlobalUser)
510
+
511
+ instance.dispose()
512
+ })
513
+ })
394
514
  })
@@ -1,3 +1,4 @@
1
+ import {type SanityUser as SanityUserFromClient} from '@sanity/client'
1
2
  import {createSelector} from 'reselect'
2
3
  import {
3
4
  catchError,
@@ -24,7 +25,7 @@ import {
24
25
  } from 'rxjs'
25
26
 
26
27
  import {getDashboardOrganizationId} from '../auth/authStore'
27
- import {getClientState} from '../client/clientStore'
28
+ import {getClient, getClientState} from '../client/clientStore'
28
29
  import {bindActionGlobally} from '../store/createActionBinder'
29
30
  import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
30
31
  import {type StoreState} from '../store/createStoreState'
@@ -42,12 +43,23 @@ import {
42
43
  updateLastLoadMoreRequest,
43
44
  } from './reducers'
44
45
  import {
46
+ type GetUserOptions,
45
47
  type GetUsersOptions,
48
+ type ResolveUserOptions,
46
49
  type ResolveUsersOptions,
50
+ type SanityUser,
47
51
  type SanityUserResponse,
48
52
  type UsersStoreState,
49
53
  } from './types'
50
- import {API_VERSION, USERS_STATE_CLEAR_DELAY} from './usersConstants'
54
+ import {API_VERSION, PROJECT_API_VERSION, USERS_STATE_CLEAR_DELAY} from './usersConstants'
55
+
56
+ /** @internal */
57
+ export type PatchedSanityUserFromClient = Omit<SanityUserFromClient, 'id'> & {
58
+ id: string
59
+ sanityUserId: string
60
+ email: string
61
+ provider: string
62
+ }
51
63
 
52
64
  /**
53
65
  * The users store resource that manages user data fetching and state.
@@ -100,9 +112,98 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
100
112
  group$.pipe(
101
113
  switchMap((e) => {
102
114
  if (!e.added) return EMPTY
103
- const {batchSize, ...options} = parseUsersKey(group$.key)
115
+ const {userId, batchSize, ...options} = parseUsersKey(group$.key)
116
+
117
+ if (userId) {
118
+ // In the future we will be able to fetch a user from the global API using the resourceUserId,
119
+ // but for now we need to use the project subdomain to fetch a user if the userId is a project user (starts with "p")
120
+ if (userId.startsWith('p')) {
121
+ const client = getClient(instance, {
122
+ apiVersion: PROJECT_API_VERSION,
123
+ // this is a global store, so we need to use the projectId from the options when we're fetching
124
+ // users from a project subdomain
125
+ projectId: options.projectId,
126
+ useProjectHostname: true,
127
+ })
104
128
 
105
- const projectId = options.projectId ?? instance.config.projectId
129
+ return client.observable
130
+ .request<PatchedSanityUserFromClient>({
131
+ method: 'GET',
132
+ uri: `/users/${userId}`,
133
+ })
134
+ .pipe(
135
+ map((user) => {
136
+ // We need to convert the user to the format we expect
137
+ const convertedUser: SanityUser = {
138
+ sanityUserId: user.sanityUserId,
139
+ profile: {
140
+ id: user.id,
141
+ displayName: user.displayName,
142
+ familyName: user.familyName ?? undefined,
143
+ givenName: user.givenName ?? undefined,
144
+ middleName: user.middleName ?? undefined,
145
+ imageUrl: user.imageUrl ?? undefined,
146
+ createdAt: user.createdAt,
147
+ updatedAt: user.updatedAt,
148
+ isCurrentUser: user.isCurrentUser,
149
+ email: user.email,
150
+ provider: user.provider,
151
+ },
152
+ memberships: [],
153
+ }
154
+ return {
155
+ data: [convertedUser],
156
+ totalCount: 1,
157
+ nextCursor: null,
158
+ }
159
+ }),
160
+ catchError((error) => {
161
+ state.set('setUsersError', setUsersError(group$.key, error))
162
+ return EMPTY
163
+ }),
164
+ tap((response) =>
165
+ state.set('setUsersData', setUsersData(group$.key, response)),
166
+ ),
167
+ )
168
+ }
169
+
170
+ // Fetch the user from the global API
171
+ const scope = userId.startsWith('g') ? 'global' : undefined
172
+ const client = getClient(instance, {
173
+ scope,
174
+ apiVersion: API_VERSION,
175
+ })
176
+ const resourceType = options.resourceType || 'project'
177
+ const resourceId =
178
+ resourceType === 'organization' ? options.organizationId : options.projectId
179
+ if (!resourceId) {
180
+ return throwError(() => new Error('An organizationId or a projectId is required'))
181
+ }
182
+ return client.observable
183
+ .request<SanityUser | SanityUserResponse>({
184
+ method: 'GET',
185
+ uri: `access/${resourceType}/${resourceId}/users/${userId}`,
186
+ })
187
+ .pipe(
188
+ map((response) => {
189
+ // If the response is a single user object (has sanityUserId), wrap it in the expected format
190
+ if ('sanityUserId' in response) {
191
+ return {
192
+ data: [response],
193
+ totalCount: 1,
194
+ nextCursor: null,
195
+ } as SanityUserResponse
196
+ }
197
+ return response as SanityUserResponse
198
+ }),
199
+ catchError((error) => {
200
+ state.set('setUsersError', setUsersError(group$.key, error))
201
+ return EMPTY
202
+ }),
203
+ tap((response) => state.set('setUsersData', setUsersData(group$.key, response))),
204
+ )
205
+ }
206
+ const projectId = options.projectId
106
207
 
107
208
  // the resource type this request will use
108
209
  // If resourceType is explicitly provided, use it
@@ -313,3 +414,30 @@ export const loadMoreUsers = bindActionGlobally(
313
414
  return await promise
314
415
  },
315
416
  )
417
+
418
+ /**
419
+ * @beta
420
+ */
421
+ export const getUserState = bindActionGlobally(
422
+ usersStore,
423
+ ({instance}, {userId, ...options}: GetUserOptions) => {
424
+ return getUsersState(instance, {userId, ...options}).observable.pipe(
425
+ map((res) => res?.data[0]),
426
+ distinctUntilChanged((a, b) => a?.profile.updatedAt === b?.profile.updatedAt),
427
+ )
428
+ },
429
+ )
430
+
431
+ /**
432
+ * @beta
433
+ */
434
+ export const resolveUser = bindActionGlobally(
435
+ usersStore,
436
+ async ({instance}, {signal, ...options}: ResolveUserOptions) => {
437
+ const result = await resolveUsers(instance, {
438
+ signal,
439
+ ...options,
440
+ })
441
+ return result?.data[0]
442
+ },
443
+ )