@sanity/sdk 2.9.0 → 2.11.0

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 (73) hide show
  1. package/dist/_chunks-dts/utils.d.ts +295 -69
  2. package/dist/_chunks-es/_internal.js +3 -14
  3. package/dist/_chunks-es/_internal.js.map +1 -1
  4. package/dist/_chunks-es/createGroqSearchFilter.js +129 -59
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  6. package/dist/_chunks-es/version.js +1 -1
  7. package/dist/_exports/_internal.d.ts +16 -2
  8. package/dist/_exports/_internal.js +3 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +275 -149
  11. package/dist/index.js.map +1 -1
  12. package/package.json +11 -15
  13. package/src/_exports/_internal.ts +1 -0
  14. package/src/_exports/index.ts +33 -2
  15. package/src/agent/agentActions.ts +21 -25
  16. package/src/client/clientStore.test.ts +24 -60
  17. package/src/client/clientStore.ts +49 -56
  18. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  19. package/src/comlink/node/actions/getOrCreateNode.test.ts +5 -2
  20. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  21. package/src/comlink/node/actions/releaseNode.test.ts +3 -3
  22. package/src/config/sanityConfig.ts +72 -13
  23. package/src/document/applyDocumentActions.test.ts +7 -7
  24. package/src/document/applyDocumentActions.ts +5 -5
  25. package/src/document/documentStore.test.ts +68 -62
  26. package/src/document/documentStore.ts +33 -38
  27. package/src/document/processActions.ts +2 -2
  28. package/src/document/reducers.ts +4 -4
  29. package/src/document/sharedListener.ts +5 -7
  30. package/src/organization/organization.test-d.ts +102 -0
  31. package/src/organization/organization.test.ts +138 -0
  32. package/src/organization/organization.ts +166 -0
  33. package/src/organizations/organizations.test-d.ts +77 -0
  34. package/src/organizations/organizations.test.ts +150 -0
  35. package/src/organizations/organizations.ts +132 -0
  36. package/src/presence/bifurTransport.test.ts +46 -6
  37. package/src/presence/bifurTransport.ts +13 -1
  38. package/src/presence/presenceStore.test.ts +101 -5
  39. package/src/presence/presenceStore.ts +96 -24
  40. package/src/preview/getPreviewState.ts +1 -1
  41. package/src/preview/previewProjectionUtils.test.ts +4 -4
  42. package/src/preview/previewProjectionUtils.ts +6 -7
  43. package/src/preview/resolvePreview.ts +5 -1
  44. package/src/project/project.test-d.ts +93 -0
  45. package/src/project/project.test.ts +108 -10
  46. package/src/project/project.ts +152 -26
  47. package/src/projection/getProjectionState.ts +4 -4
  48. package/src/projection/projectionStore.test.ts +2 -2
  49. package/src/projection/resolveProjection.ts +2 -2
  50. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  51. package/src/projection/subscribeToStateAndFetchBatches.ts +11 -15
  52. package/src/projects/projects.test-d.ts +38 -0
  53. package/src/projects/projects.test.ts +104 -38
  54. package/src/projects/projects.ts +74 -14
  55. package/src/query/queryStore.test.ts +12 -12
  56. package/src/query/queryStore.ts +10 -11
  57. package/src/query/reducers.ts +3 -3
  58. package/src/releases/getPerspectiveState.ts +5 -5
  59. package/src/releases/releasesStore.test.ts +6 -6
  60. package/src/releases/releasesStore.ts +9 -9
  61. package/src/store/createActionBinder.test.ts +31 -31
  62. package/src/store/createActionBinder.ts +43 -38
  63. package/src/store/createSanityInstance.ts +5 -6
  64. package/src/telemetry/devMode.test.ts +8 -0
  65. package/src/telemetry/devMode.ts +10 -9
  66. package/src/telemetry/initTelemetry.test.ts +0 -17
  67. package/src/telemetry/initTelemetry.ts +2 -12
  68. package/src/users/reducers.ts +3 -4
  69. package/src/utils/createFetcherStore.ts +6 -4
  70. package/src/utils/isImportError.test.ts +72 -0
  71. package/src/utils/isImportError.ts +34 -0
  72. package/src/utils/object.test.ts +95 -0
  73. package/src/utils/object.ts +142 -0
@@ -0,0 +1,150 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {of} from 'rxjs'
3
+ import {afterEach, beforeEach, describe, it} from 'vitest'
4
+
5
+ import {getClientState} from '../client/clientStore'
6
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
+ import {type StateSource} from '../store/createStateSourceAction'
8
+ import {getOrganizationsCacheKey, resolveOrganizations} from './organizations'
9
+
10
+ vi.mock('../client/clientStore')
11
+
12
+ describe('organizations', () => {
13
+ let instance: SanityInstance
14
+
15
+ beforeEach(() => {
16
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
17
+ })
18
+
19
+ afterEach(() => {
20
+ instance.dispose()
21
+ })
22
+
23
+ it('calls `client.observable.request` against `/organizations` and returns the result', async () => {
24
+ const organizations = [{id: 'org_1'}, {id: 'org_2'}]
25
+ const request = vi.fn().mockReturnValue(of(organizations))
26
+
27
+ const mockClient = {
28
+ observable: {request} as unknown as SanityClient['observable'],
29
+ } as SanityClient
30
+
31
+ vi.mocked(getClientState).mockReturnValue({
32
+ observable: of(mockClient),
33
+ } as StateSource<SanityClient>)
34
+
35
+ const result = await resolveOrganizations(instance)
36
+ expect(result).toEqual(organizations)
37
+ expect(request).toHaveBeenCalledWith({
38
+ uri: '/organizations',
39
+ query: {
40
+ includeImplicitMemberships: 'false',
41
+ includeMembers: 'false',
42
+ includeFeatures: 'false',
43
+ },
44
+ tag: 'organizations.get',
45
+ })
46
+ })
47
+
48
+ it('serializes query params (booleans → strings) and respects flags', async () => {
49
+ const request = vi.fn().mockReturnValue(of([]))
50
+ const mockClient = {
51
+ observable: {request} as unknown as SanityClient['observable'],
52
+ } as SanityClient
53
+
54
+ vi.mocked(getClientState).mockReturnValue({
55
+ observable: of(mockClient),
56
+ } as StateSource<SanityClient>)
57
+
58
+ await resolveOrganizations(instance, {
59
+ includeMembers: true,
60
+ includeFeatures: true,
61
+ includeImplicitMemberships: true,
62
+ })
63
+
64
+ expect(request).toHaveBeenCalledWith({
65
+ uri: '/organizations',
66
+ query: {
67
+ includeImplicitMemberships: 'true',
68
+ includeMembers: 'true',
69
+ includeFeatures: 'true',
70
+ },
71
+ tag: 'organizations.get',
72
+ })
73
+ })
74
+ })
75
+
76
+ describe('organizations cache key generation', () => {
77
+ let instance: SanityInstance
78
+
79
+ beforeEach(() => {
80
+ instance = createSanityInstance({})
81
+ })
82
+
83
+ afterEach(() => {
84
+ instance.dispose()
85
+ })
86
+
87
+ it('default call excludes all segments (all flags default-false)', () => {
88
+ expect(getOrganizationsCacheKey(instance)).toBe('organizations')
89
+ })
90
+
91
+ it('treats undefined and the matching default as the same key', () => {
92
+ expect(getOrganizationsCacheKey(instance)).toBe(
93
+ getOrganizationsCacheKey(instance, {
94
+ includeMembers: false,
95
+ includeFeatures: false,
96
+ includeImplicitMemberships: false,
97
+ }),
98
+ )
99
+ })
100
+
101
+ it('explicit includeMembers: true appends :members', () => {
102
+ expect(getOrganizationsCacheKey(instance, {includeMembers: true})).toBe('organizations:members')
103
+ })
104
+
105
+ it('explicit includeFeatures: true appends :features', () => {
106
+ expect(getOrganizationsCacheKey(instance, {includeFeatures: true})).toBe(
107
+ 'organizations:features',
108
+ )
109
+ })
110
+
111
+ it('explicit includeImplicitMemberships: true appends :implicit', () => {
112
+ expect(getOrganizationsCacheKey(instance, {includeImplicitMemberships: true})).toBe(
113
+ 'organizations:implicit',
114
+ )
115
+ })
116
+
117
+ it('combines all segments in order', () => {
118
+ expect(
119
+ getOrganizationsCacheKey(instance, {
120
+ includeMembers: true,
121
+ includeFeatures: true,
122
+ includeImplicitMemberships: true,
123
+ }),
124
+ ).toBe('organizations:members:features:implicit')
125
+ })
126
+
127
+ it('produces distinct keys for each meaningful option permutation', () => {
128
+ const keys = new Set([
129
+ getOrganizationsCacheKey(instance),
130
+ getOrganizationsCacheKey(instance, {includeMembers: true}),
131
+ getOrganizationsCacheKey(instance, {includeFeatures: true}),
132
+ getOrganizationsCacheKey(instance, {includeImplicitMemberships: true}),
133
+ getOrganizationsCacheKey(instance, {includeMembers: true, includeFeatures: true}),
134
+ getOrganizationsCacheKey(instance, {
135
+ includeMembers: true,
136
+ includeImplicitMemberships: true,
137
+ }),
138
+ getOrganizationsCacheKey(instance, {
139
+ includeFeatures: true,
140
+ includeImplicitMemberships: true,
141
+ }),
142
+ getOrganizationsCacheKey(instance, {
143
+ includeMembers: true,
144
+ includeFeatures: true,
145
+ includeImplicitMemberships: true,
146
+ }),
147
+ ])
148
+ expect(keys.size).toBe(8)
149
+ })
150
+ })
@@ -0,0 +1,132 @@
1
+ import {switchMap} from 'rxjs'
2
+
3
+ import {getClientState} from '../client/clientStore'
4
+ import {
5
+ type OrganizationBase,
6
+ type OrganizationMember,
7
+ type OrganizationOptions,
8
+ } from '../organization/organization'
9
+ import {type SanityInstance} from '../store/createSanityInstance'
10
+ import {type StateSource} from '../store/createStateSourceAction'
11
+ import {createFetcherStore} from '../utils/createFetcherStore'
12
+
13
+ const API_VERSION = 'v2025-02-19'
14
+
15
+ /**
16
+ * The list shape returned from `/organizations`, with `members` and/or
17
+ * `features` conditionally included based on the query options used.
18
+ * @public
19
+ */
20
+ export type Organizations<
21
+ IncludeMembers extends boolean = false,
22
+ IncludeFeatures extends boolean = false,
23
+ > = (Pick<
24
+ OrganizationBase,
25
+ | 'id'
26
+ | 'name'
27
+ | 'slug'
28
+ | 'createdAt'
29
+ | 'updatedAt'
30
+ | 'defaultRoleName'
31
+ | 'dashboardStatus'
32
+ | 'aiFeaturesStatus'
33
+ > &
34
+ // `boolean extends T` is non-distributive — true only when T is the wide
35
+ // `boolean`, in which case the field is optional. Literal `true`/`false`
36
+ // fall through to the strict branch.
37
+ (boolean extends IncludeMembers
38
+ ? {members?: OrganizationMember[]}
39
+ : IncludeMembers extends true
40
+ ? {members: OrganizationMember[]}
41
+ : unknown) &
42
+ (boolean extends IncludeFeatures
43
+ ? {features?: string[]}
44
+ : IncludeFeatures extends true
45
+ ? {features: string[]}
46
+ : unknown))[]
47
+
48
+ /** @public */
49
+ export interface OrganizationsOptions<
50
+ IncludeMembers extends boolean = false,
51
+ IncludeFeatures extends boolean = false,
52
+ > extends Omit<OrganizationOptions<IncludeMembers, IncludeFeatures>, 'organizationId'> {
53
+ /**
54
+ * When `true`, includes organisations the user has access to via
55
+ * project-level grants, not just direct organisation memberships.
56
+ */
57
+ includeImplicitMemberships?: boolean
58
+ }
59
+
60
+ function normalizeOrganizationOptions(options?: OrganizationsOptions<boolean, boolean>) {
61
+ return {
62
+ includeImplicitMemberships: options?.includeImplicitMemberships ?? false,
63
+ includeMembers: options?.includeMembers ?? false,
64
+ includeFeatures: options?.includeFeatures ?? false,
65
+ }
66
+ }
67
+
68
+ /** @internal */
69
+ export function getOrganizationsCacheKey(
70
+ _instance: SanityInstance,
71
+ options?: OrganizationsOptions<boolean, boolean>,
72
+ ): string {
73
+ const {includeMembers, includeFeatures, includeImplicitMemberships} =
74
+ normalizeOrganizationOptions(options)
75
+ const membersKey = includeMembers ? ':members' : ''
76
+ const featuresKey = includeFeatures ? ':features' : ''
77
+ const implicitKey = includeImplicitMemberships ? ':implicit' : ''
78
+ return `organizations${membersKey}${featuresKey}${implicitKey}`
79
+ }
80
+
81
+ const organizations = createFetcherStore({
82
+ name: 'Organizations',
83
+ getKey: getOrganizationsCacheKey,
84
+ fetcher: (instance) => (options?: OrganizationsOptions<boolean, boolean>) => {
85
+ return getClientState(instance, {
86
+ apiVersion: API_VERSION,
87
+ scope: 'global',
88
+ }).observable.pipe(
89
+ switchMap((client) => {
90
+ const normalized = normalizeOrganizationOptions(options)
91
+ const query = Object.fromEntries(
92
+ Object.entries(normalized)
93
+ .filter(([, value]) => value !== undefined)
94
+ .map(([key, value]) => [key, String(value)]),
95
+ )
96
+
97
+ return client.observable.request({
98
+ uri: `/organizations`,
99
+ query,
100
+ tag: 'organizations.get',
101
+ })
102
+ }),
103
+ )
104
+ },
105
+ })
106
+
107
+ /**
108
+ * Public signature for the organization state source. The conditional generics
109
+ * cannot flow through `BoundStoreAction`, so we declare the signature here
110
+ * and assign the (already-correct) runtime function to it.
111
+ */
112
+ type GetOrganizationsState = <
113
+ IncludeMembers extends boolean = false,
114
+ IncludeFeatures extends boolean = false,
115
+ >(
116
+ instance: SanityInstance,
117
+ options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
118
+ ) => StateSource<Organizations<IncludeMembers, IncludeFeatures> | undefined>
119
+
120
+ type ResolveOrganizations = <
121
+ IncludeMembers extends boolean = false,
122
+ IncludeFeatures extends boolean = false,
123
+ >(
124
+ instance: SanityInstance,
125
+ options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
126
+ ) => Promise<Organizations<IncludeMembers, IncludeFeatures>>
127
+
128
+ /** @public */
129
+ export const getOrganizationsState: GetOrganizationsState = organizations.getState
130
+
131
+ /** @public */
132
+ export const resolveOrganizations: ResolveOrganizations = organizations.resolveState
@@ -45,16 +45,18 @@ describe('createBifurTransport', () => {
45
45
 
46
46
  beforeEach(() => {
47
47
  vi.useFakeTimers()
48
+ vi.clearAllMocks()
48
49
  mockBifurClient = {
49
50
  listen: vi.fn(() => new Subject<never>()),
50
51
  request: vi.fn(() => of(undefined)),
51
52
  }
52
53
  fromUrlMock.mockReturnValue(mockBifurClient)
53
54
 
55
+ // Default mock is a dataset client using project hostname
54
56
  mockSanityClient = {
55
57
  config: () => ({
56
58
  dataset: 'test-dataset',
57
- url: 'http://localhost:3333',
59
+ url: 'https://test-project.api.sanity.io/v2022-06-30',
58
60
  requestTagPrefix: 'test-tag',
59
61
  }),
60
62
  withConfig: vi.fn().mockReturnThis(),
@@ -63,7 +65,7 @@ describe('createBifurTransport', () => {
63
65
  token$ = new Subject<string | null>()
64
66
  })
65
67
 
66
- it('constructs the bifur client with the correct URL', () => {
68
+ it('constructs the bifur client URL for a dataset resource', () => {
67
69
  createBifurTransport({
68
70
  client: mockSanityClient,
69
71
  token$,
@@ -71,13 +73,51 @@ describe('createBifurTransport', () => {
71
73
  })
72
74
 
73
75
  expect(fromUrlMock).toHaveBeenCalledWith(
74
- 'ws://localhost:3333/socket/test-dataset?tag=test-tag',
75
- {
76
- token$,
77
- },
76
+ 'wss://test-project.api.sanity.io/v2022-06-30/socket/test-dataset?tag=test-tag',
77
+ {token$},
78
+ )
79
+ })
80
+
81
+ it('constructs the bifur client URL for a canvas resource', () => {
82
+ const canvasClient = {
83
+ config: () => ({
84
+ resource: {type: 'canvas', id: 'canvas-123'},
85
+ url: 'https://api.sanity.io/v2022-06-30',
86
+ requestTagPrefix: 'test-tag',
87
+ }),
88
+ withConfig: vi.fn().mockReturnThis(),
89
+ } as unknown as SanityClient
90
+
91
+ createBifurTransport({
92
+ client: canvasClient,
93
+ token$,
94
+ sessionId: 'session-id-123',
95
+ })
96
+
97
+ expect(fromUrlMock).toHaveBeenCalledWith(
98
+ 'wss://api.sanity.io/v2022-06-30/socket/canvases/canvas-123?tag=test-tag',
99
+ {token$},
78
100
  )
79
101
  })
80
102
 
103
+ it('throws when no canvas resource or dataset is configured', () => {
104
+ const invalidClient = {
105
+ config: () => ({
106
+ url: 'https://api.sanity.io/v2022-06-30',
107
+ requestTagPrefix: 'test-tag',
108
+ }),
109
+ withConfig: vi.fn().mockReturnThis(),
110
+ } as unknown as SanityClient
111
+
112
+ expect(() =>
113
+ createBifurTransport({
114
+ client: invalidClient,
115
+ token$,
116
+ sessionId: 'session-id-123',
117
+ }),
118
+ ).toThrow('Unable to determine presence URL')
119
+ })
120
+
81
121
  it('handles incoming rollCall events', () => {
82
122
  const incomingBifurEvents$ = new Subject<IncomingBifurEvent>()
83
123
  mockBifurClient.listen.mockReturnValue(incomingBifurEvents$)
@@ -37,11 +37,23 @@ type IncomingBifurEvent = RollCallEvent | BifurStateMessage | BifurDisconnectMes
37
37
  function getBifurClient(client: SanityClient, token$: Observable<string | null>): BifurClient {
38
38
  const bifurVersionedClient = client.withConfig({apiVersion: '2022-06-30'})
39
39
  const {
40
+ resource,
40
41
  dataset,
41
42
  url: baseUrl,
42
43
  requestTagPrefix = 'sanity.sdk.presence',
43
44
  } = bifurVersionedClient.config()
44
- const url = `${baseUrl.replace(/\/+$/, '')}/socket/${dataset}`.replace(/^http/, 'ws')
45
+
46
+ let resourcePath: string
47
+ if (resource?.type === 'canvas') {
48
+ resourcePath = `canvases/${resource.id}`
49
+ } else if (dataset) {
50
+ // Dataset clients use project hostname — dataset name alone is the socket path
51
+ resourcePath = dataset
52
+ } else {
53
+ throw new Error(`Unable to determine presence URL: no canvas resource or dataset configured`)
54
+ }
55
+
56
+ const url = `${baseUrl}/socket/${resourcePath}`.replace(/^http/, 'ws')
45
57
 
46
58
  const urlWithTag = `${url}?tag=${requestTagPrefix}`
47
59
 
@@ -1,6 +1,6 @@
1
1
  import {type SanityClient} from '@sanity/client'
2
- import {delay, firstValueFrom, of, Subject} from 'rxjs'
3
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
2
+ import {delay, firstValueFrom, type Observable, of, Subject} from 'rxjs'
3
+ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
4
4
 
5
5
  import {getTokenState} from '../auth/authStore'
6
6
  import {getClient} from '../client/clientStore'
@@ -9,7 +9,7 @@ import {type SanityUser} from '../users/types'
9
9
  import {getUserState} from '../users/usersStore'
10
10
  import {createBifurTransport} from './bifurTransport'
11
11
  import {getPresence} from './presenceStore'
12
- import {type PresenceLocation, type TransportEvent} from './types'
12
+ import {type PresenceLocation, type TransportEvent, type TransportMessage} from './types'
13
13
 
14
14
  vi.mock('../auth/authStore')
15
15
  vi.mock('../client/clientStore')
@@ -21,8 +21,8 @@ describe('presenceStore', () => {
21
21
  let mockClient: SanityClient
22
22
  let mockTokenState: Subject<string | null>
23
23
  let mockIncomingEvents: Subject<TransportEvent>
24
- let mockDispatchMessage: ReturnType<typeof vi.fn>
25
- let mockGetUserState: ReturnType<typeof vi.fn>
24
+ let mockDispatchMessage: Mock<(message: TransportMessage) => Observable<void>>
25
+ let mockGetUserState: Mock<typeof getUserState>
26
26
 
27
27
  const mockUser: SanityUser = {
28
28
  sanityUserId: 'u123',
@@ -48,6 +48,9 @@ describe('presenceStore', () => {
48
48
 
49
49
  mockClient = {
50
50
  withConfig: vi.fn().mockReturnThis(),
51
+ observable: {
52
+ request: vi.fn(() => of({organizationId: 'test-org-id'})),
53
+ },
51
54
  } as unknown as SanityClient
52
55
 
53
56
  mockTokenState = new Subject<string | null>()
@@ -243,5 +246,98 @@ describe('presenceStore', () => {
243
246
 
244
247
  unsubscribe()
245
248
  })
249
+
250
+ it('should throw an error when initialized with a media library resource', () => {
251
+ const mediaLibraryResource = {mediaLibraryId: 'ml123'}
252
+
253
+ expect(() => {
254
+ getPresence(instance, {resource: mediaLibraryResource})
255
+ }).toThrow('Presence is not supported for media library resources.')
256
+ })
257
+
258
+ it('should work with a dataset resource', () => {
259
+ const datasetResource = {projectId: 'test-project', dataset: 'test-dataset'}
260
+
261
+ expect(() => {
262
+ getPresence(instance, {resource: datasetResource})
263
+ }).not.toThrow()
264
+ })
265
+
266
+ it('should work with a canvas resource', () => {
267
+ const canvasResource = {canvasId: 'canvas123'}
268
+
269
+ expect(() => {
270
+ getPresence(instance, {resource: canvasResource})
271
+ }).not.toThrow()
272
+ })
273
+
274
+ it('creates a project-hostname client for dataset resources', () => {
275
+ getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
276
+
277
+ expect(getClient).toHaveBeenCalledWith(instance, {
278
+ apiVersion: '2026-03-30',
279
+ projectId: 'my-project',
280
+ dataset: 'my-dataset',
281
+ useProjectHostname: true,
282
+ })
283
+ })
284
+
285
+ it('creates a resource client for canvas resources', () => {
286
+ const canvasResource = {canvasId: 'canvas123'}
287
+ getPresence(instance, {resource: canvasResource})
288
+
289
+ expect(getClient).toHaveBeenCalledWith(instance, {
290
+ apiVersion: '2026-03-30',
291
+ resource: canvasResource,
292
+ })
293
+ })
294
+
295
+ it('fetches organizationId from canvas endpoint for canvas resources', () => {
296
+ const canvasResource = {canvasId: 'canvas123'}
297
+ getPresence(instance, {resource: canvasResource})
298
+
299
+ expect(mockClient.observable.request).toHaveBeenCalledWith({
300
+ uri: '/canvases/canvas123',
301
+ tag: 'canvases.get',
302
+ })
303
+ })
304
+
305
+ it('does not fetch organizationId for dataset resources', () => {
306
+ getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
307
+
308
+ expect(mockClient.observable.request).not.toHaveBeenCalled()
309
+ })
310
+
311
+ it('fetches user data for canvas users', async () => {
312
+ const source = getPresence(instance, {resource: {canvasId: 'canvas123'}})
313
+ const unsubscribe = source.subscribe(() => {})
314
+
315
+ await firstValueFrom(of(null).pipe(delay(10)))
316
+
317
+ mockIncomingEvents.next({
318
+ type: 'state',
319
+ userId: 'user-1',
320
+ sessionId: 'other-session',
321
+ timestamp: '2023-01-01T12:00:00Z',
322
+ locations: [
323
+ {
324
+ type: 'document',
325
+ documentId: 'doc-1',
326
+ path: ['title'],
327
+ lastActiveAt: '2023-01-01T12:00:00Z',
328
+ },
329
+ ],
330
+ })
331
+
332
+ await firstValueFrom(of(null).pipe(delay(50)))
333
+
334
+ expect(getUserState).toHaveBeenCalledWith(instance, {
335
+ userId: 'user-1',
336
+ resourceType: 'organization',
337
+ organizationId: 'test-org-id',
338
+ })
339
+
340
+ unsubscribe()
341
+ })
246
342
  })
247
343
  })