@sanity/sdk 0.0.0-alpha.21 → 0.0.0-alpha.23

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 (127) hide show
  1. package/dist/index.d.ts +428 -325
  2. package/dist/index.js +1618 -1553
  3. package/dist/index.js.map +1 -1
  4. package/package.json +6 -7
  5. package/src/_exports/index.ts +31 -30
  6. package/src/auth/authStore.test.ts +149 -104
  7. package/src/auth/authStore.ts +51 -100
  8. package/src/auth/handleAuthCallback.test.ts +67 -34
  9. package/src/auth/handleAuthCallback.ts +8 -7
  10. package/src/auth/logout.test.ts +61 -29
  11. package/src/auth/logout.ts +26 -28
  12. package/src/auth/refreshStampedToken.test.ts +9 -9
  13. package/src/auth/refreshStampedToken.ts +62 -56
  14. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
  15. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
  16. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
  17. package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
  18. package/src/client/clientStore.test.ts +131 -67
  19. package/src/client/clientStore.ts +117 -116
  20. package/src/comlink/controller/actions/destroyController.test.ts +38 -13
  21. package/src/comlink/controller/actions/destroyController.ts +11 -15
  22. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
  23. package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
  24. package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
  25. package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
  26. package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
  27. package/src/comlink/controller/actions/releaseChannel.ts +22 -21
  28. package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
  29. package/src/comlink/controller/comlinkControllerStore.ts +44 -5
  30. package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
  31. package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
  32. package/src/comlink/node/actions/releaseNode.test.ts +75 -55
  33. package/src/comlink/node/actions/releaseNode.ts +19 -21
  34. package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
  35. package/src/comlink/node/comlinkNodeStore.ts +22 -5
  36. package/src/config/authConfig.ts +79 -0
  37. package/src/config/sanityConfig.ts +48 -0
  38. package/src/datasets/datasets.test.ts +2 -2
  39. package/src/datasets/datasets.ts +18 -5
  40. package/src/document/actions.test.ts +22 -10
  41. package/src/document/actions.ts +44 -56
  42. package/src/document/applyDocumentActions.test.ts +96 -36
  43. package/src/document/applyDocumentActions.ts +140 -99
  44. package/src/document/documentStore.test.ts +103 -155
  45. package/src/document/documentStore.ts +247 -237
  46. package/src/document/listen.ts +56 -55
  47. package/src/document/patchOperations.ts +0 -43
  48. package/src/document/permissions.test.ts +25 -12
  49. package/src/document/permissions.ts +11 -4
  50. package/src/document/processActions.test.ts +41 -8
  51. package/src/document/reducers.test.ts +87 -16
  52. package/src/document/reducers.ts +2 -2
  53. package/src/document/sharedListener.test.ts +34 -16
  54. package/src/document/sharedListener.ts +33 -11
  55. package/src/preview/getPreviewState.test.ts +40 -39
  56. package/src/preview/getPreviewState.ts +68 -56
  57. package/src/preview/previewConstants.ts +43 -0
  58. package/src/preview/previewQuery.test.ts +1 -1
  59. package/src/preview/previewQuery.ts +4 -5
  60. package/src/preview/previewStore.test.ts +13 -58
  61. package/src/preview/previewStore.ts +7 -21
  62. package/src/preview/resolvePreview.test.ts +33 -104
  63. package/src/preview/resolvePreview.ts +11 -21
  64. package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
  65. package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
  66. package/src/preview/util.ts +1 -0
  67. package/src/project/project.test.ts +3 -3
  68. package/src/project/project.ts +28 -5
  69. package/src/projection/getProjectionState.test.ts +69 -49
  70. package/src/projection/getProjectionState.ts +42 -50
  71. package/src/projection/projectionQuery.ts +1 -1
  72. package/src/projection/projectionStore.test.ts +13 -51
  73. package/src/projection/projectionStore.ts +6 -18
  74. package/src/projection/resolveProjection.test.ts +32 -127
  75. package/src/projection/resolveProjection.ts +15 -28
  76. package/src/projection/subscribeToStateAndFetchBatches.test.ts +105 -90
  77. package/src/projection/subscribeToStateAndFetchBatches.ts +94 -81
  78. package/src/projection/util.ts +2 -0
  79. package/src/projects/projects.test.ts +13 -4
  80. package/src/projects/projects.ts +6 -1
  81. package/src/query/queryStore.test.ts +10 -47
  82. package/src/query/queryStore.ts +151 -133
  83. package/src/query/queryStoreConstants.ts +2 -0
  84. package/src/store/createActionBinder.test.ts +153 -0
  85. package/src/store/createActionBinder.ts +176 -0
  86. package/src/store/createSanityInstance.test.ts +84 -0
  87. package/src/store/createSanityInstance.ts +124 -0
  88. package/src/store/createStateSourceAction.test.ts +196 -0
  89. package/src/store/createStateSourceAction.ts +260 -0
  90. package/src/store/createStoreInstance.test.ts +81 -0
  91. package/src/store/createStoreInstance.ts +80 -0
  92. package/src/store/createStoreState.test.ts +85 -0
  93. package/src/store/createStoreState.ts +92 -0
  94. package/src/store/defineStore.test.ts +18 -0
  95. package/src/store/defineStore.ts +81 -0
  96. package/src/users/reducers.test.ts +318 -0
  97. package/src/users/reducers.ts +88 -0
  98. package/src/users/types.ts +46 -4
  99. package/src/users/usersConstants.ts +4 -0
  100. package/src/users/usersStore.test.ts +350 -223
  101. package/src/users/usersStore.ts +285 -149
  102. package/src/utils/createFetcherStore.test.ts +6 -7
  103. package/src/utils/createFetcherStore.ts +150 -153
  104. package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
  105. package/src/auth/fetchLoginUrls.test.ts +0 -163
  106. package/src/auth/fetchLoginUrls.ts +0 -74
  107. package/src/common/createLiveEventSubscriber.test.ts +0 -121
  108. package/src/common/createLiveEventSubscriber.ts +0 -55
  109. package/src/common/types.ts +0 -4
  110. package/src/instance/identity.test.ts +0 -46
  111. package/src/instance/identity.ts +0 -29
  112. package/src/instance/sanityInstance.test.ts +0 -77
  113. package/src/instance/sanityInstance.ts +0 -57
  114. package/src/instance/types.ts +0 -37
  115. package/src/preview/getPreviewProjection.ts +0 -45
  116. package/src/resources/README.md +0 -370
  117. package/src/resources/createAction.test.ts +0 -101
  118. package/src/resources/createAction.ts +0 -44
  119. package/src/resources/createResource.test.ts +0 -112
  120. package/src/resources/createResource.ts +0 -102
  121. package/src/resources/createStateSourceAction.test.ts +0 -114
  122. package/src/resources/createStateSourceAction.ts +0 -83
  123. package/src/resources/createStore.test.ts +0 -67
  124. package/src/resources/createStore.ts +0 -46
  125. package/src/store/createStore.test.ts +0 -108
  126. package/src/store/createStore.ts +0 -106
  127. /package/src/{common/util.ts → utils/hashString.ts} +0 -0
@@ -1,87 +1,151 @@
1
- import {first, firstValueFrom, map, ReplaySubject} from 'rxjs'
2
- import {describe, expect, it} from 'vitest'
1
+ import {createClient, type SanityClient} from '@sanity/client'
2
+ import {Subject} from 'rxjs'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
4
 
4
5
  import {getTokenState} from '../auth/authStore'
5
- import {createSanityInstance} from '../instance/sanityInstance'
6
- import {type StateSource} from '../resources/createStateSourceAction'
7
- import {type ClientOptions, getClient, getClientState} from './clientStore'
6
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
+ import {getClient, getClientState} from './clientStore'
8
8
 
9
- let token$: ReplaySubject<string>
9
+ // Mock dependencies
10
+ vi.mock('@sanity/client')
10
11
 
11
- vi.mock('../auth/authStore', () => {
12
- const subject = new ReplaySubject(1)
13
- subject.next('initial-token')
12
+ vi.mock('../auth/authStore')
14
13
 
15
- return {
16
- getTokenState: vi.fn().mockReturnValue({observable: subject}),
17
- }
18
- })
14
+ let instance: SanityInstance
19
15
 
20
16
  beforeEach(() => {
21
- token$ = (getTokenState as () => StateSource<string>)().observable as ReplaySubject<string>
17
+ vi.resetAllMocks()
18
+ vi.mocked(getTokenState).mockReturnValue({
19
+ getCurrent: vi.fn().mockReturnValue('initial-token'),
20
+ subscribe: vi.fn(),
21
+ observable: new Subject(),
22
+ })
23
+ vi.mocked(createClient).mockImplementation(
24
+ (clientConfig) => ({config: () => clientConfig}) as SanityClient,
25
+ )
26
+ instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
22
27
  })
23
28
 
24
- describe('getClient', () => {
25
- it('memoizes the resulting client based on current default client', () => {
26
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
27
- const client1 = getClient(instance, {apiVersion: 'vX'})
28
- const client2 = getClient(instance, {apiVersion: 'vX'})
29
- expect(client1).toBe(client2)
30
- })
29
+ afterEach(() => {
30
+ instance.dispose()
31
+ })
31
32
 
32
- it('configures `apiHost`', () => {
33
- const stagingApiHost = 'https://api.sanity.work'
34
- const instance = createSanityInstance({
35
- projectId: 'p',
36
- dataset: 'd',
37
- auth: {apiHost: stagingApiHost},
33
+ describe('clientStore', () => {
34
+ describe('getClient', () => {
35
+ it('should create a client with default configuration', () => {
36
+ const client = getClient(instance, {apiVersion: '2024-11-12'})
37
+
38
+ const defaultConfiguration = {
39
+ useCdn: false,
40
+ ignoreBrowserTokenWarning: true,
41
+ allowReconfigure: false,
42
+ requestTagPrefix: 'sanity.sdk',
43
+ projectId: 'test-project',
44
+ dataset: 'test-dataset',
45
+ token: 'initial-token',
46
+ }
47
+
48
+ expect(vi.mocked(createClient)).toHaveBeenCalledWith({
49
+ ...defaultConfiguration,
50
+ apiVersion: '2024-11-12',
51
+ })
52
+ expect(client.config()).toEqual({
53
+ ...defaultConfiguration,
54
+ apiVersion: '2024-11-12',
55
+ })
38
56
  })
39
- const projectClient = getClient(instance, {apiVersion: 'vX', scope: 'project'})
40
- const globalClient = getClient(instance, {apiVersion: 'vX', scope: 'global'})
41
57
 
42
- expect(projectClient.config().apiHost).toBe(stagingApiHost)
43
- expect(globalClient.config().apiHost).toBe(stagingApiHost)
44
- })
45
-
46
- it('returns a different result if the token is updated', () => {
47
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
48
- const client1 = getClient(instance, {apiVersion: 'vX'})
49
- const client2 = getClient(instance, {apiVersion: 'vX'})
50
- expect(client1).toBe(client2)
51
- expect(client1.config().token).toBe('initial-token')
52
- expect(client1.config().token).toBe(client2.config().token)
53
-
54
- token$.next('updated-token')
55
- const client3 = getClient(instance, {apiVersion: 'vX'})
56
- const client4 = getClient(instance, {apiVersion: 'vX'})
57
- expect(client3).toBe(client4)
58
- expect(client3.config().token).toBe('updated-token')
59
- expect(client3.config().token).toBe(client4.config().token)
60
- })
61
- })
58
+ it('should throw when using disallowed configuration keys', () => {
59
+ expect(() =>
60
+ getClient(instance, {
61
+ apiVersion: '2024-11-12',
62
+ // @ts-expect-error Testing invalid key
63
+ illegalKey: 'foo',
64
+ }),
65
+ ).toThrowError(/unsupported properties: illegalKey/)
66
+ })
62
67
 
63
- describe('getClientState', () => {
64
- it('returns a state source that updates when `getClient` updates', async () => {
65
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
66
- const options: ClientOptions = {apiVersion: 'vX', scope: 'global'}
67
- const clientState = getClientState(instance, options)
68
+ it('should reuse clients with identical configurations', () => {
69
+ const options = {apiVersion: '2024-11-12', useCdn: true}
70
+ const client1 = getClient(instance, options)
71
+ const client2 = getClient(instance, options)
68
72
 
69
- expect(clientState.getCurrent()).toBe(getClient(instance, options))
73
+ expect(client1).toBe(client2)
74
+ expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1)
75
+ })
70
76
 
71
- const subscriber = vi.fn()
72
- const unsubscribe = clientState.subscribe(subscriber)
77
+ it('should create new clients when configuration changes', () => {
78
+ const client1 = getClient(instance, {apiVersion: '2024-11-12'})
79
+ const client2 = getClient(instance, {apiVersion: '2023-08-01'})
73
80
 
74
- const tokenUpdated = firstValueFrom(
75
- clientState.observable.pipe(
76
- map((client) => client.config().token),
77
- first((token) => token === 'updated-token'),
78
- ),
79
- )
81
+ expect(client1).not.toBe(client2)
82
+ expect(vi.mocked(createClient)).toHaveBeenCalledTimes(2)
83
+ })
84
+ })
80
85
 
81
- token$.next('updated-token')
82
- await tokenUpdated
86
+ describe('token handling', () => {
87
+ it('should reset clients when token changes', () => {
88
+ // Initial client with first token
89
+ const tokenState = getTokenState(instance)
90
+ vi.mocked(tokenState.getCurrent).mockReturnValue('first-token')
91
+ const client1 = getClient(instance, {apiVersion: '2024-11-12'})
92
+
93
+ // Simulate token change
94
+ vi.mocked(tokenState.getCurrent).mockReturnValue('new-token')
95
+ const token$ = tokenState.observable as Subject<string>
96
+ token$.next('new-token')
97
+
98
+ // New client should be created with new token
99
+ const client2 = getClient(instance, {apiVersion: '2024-11-12'})
100
+
101
+ expect(client1).not.toBe(client2)
102
+ expect(vi.mocked(createClient)).toHaveBeenCalledWith(
103
+ expect.objectContaining({
104
+ token: 'new-token',
105
+ }),
106
+ )
107
+ })
108
+ })
83
109
 
84
- expect(subscriber).toHaveBeenCalledTimes(1)
85
- unsubscribe()
110
+ describe('getClientState', () => {
111
+ it('should provide a state source that emits client changes', async () => {
112
+ // Get initial client state with a specific configuration
113
+ const state = getClientState(instance, {apiVersion: '2024-11-12'})
114
+
115
+ // Get initial client
116
+ const initialClient = state.getCurrent()
117
+ expect(initialClient).toBeDefined()
118
+
119
+ // Setup a spy to track emissions from the observable
120
+ const nextSpy = vi.fn()
121
+ const subscription = state.observable.subscribe(nextSpy)
122
+
123
+ // Should have emitted once initially
124
+ expect(nextSpy).toHaveBeenCalledTimes(1)
125
+ expect(nextSpy).toHaveBeenCalledWith(initialClient)
126
+
127
+ // Simulate token change
128
+ const tokenState = getTokenState(instance)
129
+ vi.mocked(tokenState.getCurrent).mockReturnValue('updated-token')
130
+ const token$ = tokenState.observable as Subject<string>
131
+ token$.next('updated-token')
132
+
133
+ // Should emit a new client instance
134
+ expect(nextSpy).toHaveBeenCalledTimes(2)
135
+
136
+ // The new client should be different from the initial one
137
+ const updatedClient = nextSpy.mock.calls[1][0]
138
+ expect(updatedClient).not.toBe(initialClient)
139
+
140
+ // The updated client should have the new token
141
+ expect(updatedClient.config()).toEqual(
142
+ expect.objectContaining({
143
+ token: 'updated-token',
144
+ }),
145
+ )
146
+
147
+ // Clean up subscription
148
+ subscription.unsubscribe()
149
+ })
86
150
  })
87
151
  })
@@ -1,23 +1,61 @@
1
1
  import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'
2
- import {createSelector} from 'reselect'
2
+ import {pick} from 'lodash-es'
3
3
 
4
4
  import {getTokenState} from '../auth/authStore'
5
- import {type ResourceId} from '../document/patchOperations'
6
- import {type SanityInstance} from '../instance/types'
7
- import {type ActionContext, createAction, createInternalAction} from '../resources/createAction'
8
- import {createResource, type Resource} from '../resources/createResource'
9
- import {createStateSourceAction} from '../resources/createStateSourceAction'
5
+ import {type DatasetHandle} from '../config/sanityConfig'
6
+ import {bindActionGlobally} from '../store/createActionBinder'
7
+ import {createStateSourceAction} from '../store/createStateSourceAction'
8
+ import {defineStore, type StoreContext} from '../store/defineStore'
10
9
 
11
10
  const DEFAULT_API_VERSION = '2024-11-12'
12
11
  const DEFAULT_REQUEST_TAG_PREFIX = 'sanity.sdk'
13
12
 
13
+ type AllowedClientConfigKey =
14
+ | 'useCdn'
15
+ | 'token'
16
+ | 'perspective'
17
+ | 'apiHost'
18
+ | 'proxy'
19
+ | 'withCredentials'
20
+ | 'timeout'
21
+ | 'maxRetries'
22
+ | 'dataset'
23
+ | 'projectId'
24
+ | 'requestTagPrefix'
25
+ | 'useProjectHostname'
26
+
27
+ const allowedKeys = Object.keys({
28
+ apiHost: null,
29
+ useCdn: null,
30
+ token: null,
31
+ perspective: null,
32
+ proxy: null,
33
+ withCredentials: null,
34
+ timeout: null,
35
+ maxRetries: null,
36
+ dataset: null,
37
+ projectId: null,
38
+ scope: null,
39
+ apiVersion: null,
40
+ requestTagPrefix: null,
41
+ useProjectHostname: null,
42
+ } satisfies Record<keyof ClientOptions, null>) as (keyof ClientOptions)[]
43
+
44
+ const DEFAULT_CLIENT_CONFIG: ClientConfig = {
45
+ apiVersion: DEFAULT_API_VERSION,
46
+ useCdn: false,
47
+ ignoreBrowserTokenWarning: true,
48
+ allowReconfigure: false,
49
+ requestTagPrefix: DEFAULT_REQUEST_TAG_PREFIX,
50
+ }
51
+
14
52
  /**
15
53
  * States tracked by the client store
16
54
  * @public
17
55
  */
18
- export interface ClientState {
19
- defaultClient: SanityClient
20
- defaultGlobalClient: SanityClient
56
+ export interface ClientStoreState {
57
+ token: string | null
58
+ clients: {[TKey in string]?: SanityClient}
21
59
  }
22
60
 
23
61
  /**
@@ -30,150 +68,113 @@ export interface ClientState {
30
68
  * ('project') and the global client ('global'). When set to `'global'`, the
31
69
  * global client is used.
32
70
  *
33
- * These options are utilized by `getClient` and `getClientState` to return a memoized
34
- * client instance, ensuring that clients are reused for identical configurations and that
35
- * updates (such as auth token changes) propagate correctly.
71
+ * These options are utilized by `getClient` and `getClientState` to configure and
72
+ * return appropriate client instances that automatically handle authentication
73
+ * updates and configuration changes.
36
74
  *
37
75
  * @public
38
76
  */
39
- export interface ClientOptions extends ClientConfig {
77
+ export interface ClientOptions extends Pick<ClientConfig, AllowedClientConfigKey>, DatasetHandle {
40
78
  /**
41
- * An optional flag to choose between the project-specific client ('project')
79
+ * An optional flag to choose between the default client (typically project-level)
42
80
  * and the global client ('global'). When set to `'global'`, the global client
43
81
  * is used.
44
82
  */
45
- scope?: 'project' | 'global'
83
+ scope?: 'default' | 'global'
46
84
  /**
47
85
  * A required string indicating the API version for the client.
48
86
  */
49
87
  apiVersion: string
50
-
51
- /**
52
- * A resource identifier for a document, in the format of `projectId.dataset`
53
- */
54
- resourceId?: ResourceId
55
88
  }
56
89
 
57
- const clientStore: Resource<ClientState> = createResource({
90
+ const clientStore = defineStore<ClientStoreState>({
58
91
  name: 'clientStore',
59
92
 
60
- getInitialState: (instance: SanityInstance) => {
61
- const {identity, config} = instance
62
- const defaultClient = createClient({
63
- projectId: identity.projectId,
64
- dataset: identity.dataset,
65
- token: config?.auth?.token,
66
- useCdn: false,
67
- apiVersion: DEFAULT_API_VERSION,
68
- requestTagPrefix: DEFAULT_REQUEST_TAG_PREFIX,
69
- ...(config?.auth?.apiHost ? {apiHost: config.auth.apiHost} : {}),
70
- })
71
-
72
- const defaultGlobalClient = createClient({
73
- token: config?.auth?.token,
74
- useCdn: false,
75
- apiVersion: 'vX', // Many global APIs are only available under this version, we may need to support other versions in the future
76
- useProjectHostname: false,
77
- requestTagPrefix: DEFAULT_REQUEST_TAG_PREFIX,
78
- ...(config?.auth?.apiHost ? {apiHost: config.auth.apiHost} : {}),
79
- })
80
-
81
- return {
82
- defaultClient,
83
- defaultGlobalClient,
84
- }
85
- },
93
+ getInitialState: (instance) => ({
94
+ clients: {},
95
+ token: getTokenState(instance).getCurrent(),
96
+ }),
86
97
 
87
- initialize() {
88
- const authEventSubscription = subscribeToAuthEvents(this)
89
- return () => {
90
- authEventSubscription.unsubscribe()
91
- }
98
+ initialize(context) {
99
+ const subscription = listenToToken(context)
100
+ return () => subscription.unsubscribe()
92
101
  },
93
102
  })
94
103
 
95
- const receiveToken = (prev: ClientState, token: string | undefined): ClientState => {
96
- const newDefaultClient = prev.defaultClient.withConfig({
97
- token,
98
- })
99
- const newGlobalClient = prev.defaultGlobalClient.withConfig({
100
- token,
101
- })
102
-
103
- return {
104
- defaultClient: newDefaultClient,
105
- defaultGlobalClient: newGlobalClient,
106
- }
107
- }
108
-
109
104
  /**
110
105
  * Updates the client store state when a token is received.
111
106
  * @internal
112
107
  */
113
- const subscribeToAuthEvents = createInternalAction(
114
- ({instance, state}: ActionContext<ClientState>) => {
115
- return () => {
116
- return getTokenState(instance).observable.subscribe((newToken) => {
117
- state.set('receiveToken', (prev) => receiveToken(prev, newToken ?? undefined))
118
- })
119
- }
120
- },
121
- )
122
-
123
- const optionsCache = new WeakMap<SanityClient, Map<string, ClientOptions>>()
124
-
125
- const defaultClientSelector = (state: ClientState, options: ClientOptions) =>
126
- options?.scope === 'global' ? state.defaultGlobalClient : state.defaultClient
127
-
128
- const memoizedOptionsSelector = createSelector(
129
- [defaultClientSelector, (_state: ClientState, options: ClientOptions) => options],
130
- (client, options) => {
131
- let nestedCache = optionsCache.get(client)
132
- if (!nestedCache) {
133
- nestedCache = new Map<string, ClientOptions>()
134
- optionsCache.set(client, nestedCache)
135
- }
136
-
137
- const key = JSON.stringify(options)
138
- const cached = nestedCache.get(key)
139
- if (cached) return cached
140
-
141
- nestedCache.set(key, options)
142
- return options
143
- },
144
- )
108
+ const listenToToken = ({instance, state}: StoreContext<ClientStoreState>) => {
109
+ return getTokenState(instance).observable.subscribe((token) => {
110
+ state.set('setTokenAndResetClients', {token, clients: {}})
111
+ })
112
+ }
145
113
 
146
- const clientSelector = createSelector(
147
- [defaultClientSelector, memoizedOptionsSelector],
148
- (client, options) => client.withConfig(options),
149
- )
114
+ const getClientConfigKey = (options: ClientOptions) => JSON.stringify(pick(options, ...allowedKeys))
150
115
 
151
116
  /**
152
- * Retrieves a memoized Sanity client instance configured with the provided options.
117
+ * Retrieves a Sanity client instance configured with the provided options.
153
118
  *
154
- * This function uses a memoized selector to return a client instance from the
155
- * client store, based on the default project or global client. The selector
156
- * leverages a WeakMap-based cache and reselect to ensure that clients with the
157
- * same configuration are reused, and that updates, such as authentication token
158
- * changes, propagate automatically.
119
+ * This function returns a client instance configured for the project or as a
120
+ * global client based on the options provided. It ensures efficient reuse of
121
+ * client instances by returning the same instance for the same options.
122
+ * For automatic handling of authentication token updates, consider using
123
+ * `getClientState`.
159
124
  *
160
125
  * @public
161
126
  */
162
- export const getClient = createAction(
127
+ export const getClient = bindActionGlobally(
163
128
  clientStore,
164
- ({state}) =>
165
- (options: ClientOptions) =>
166
- clientSelector(state.get(), options),
129
+ ({state, instance}, options: ClientOptions) => {
130
+ // Check for disallowed keys
131
+ const providedKeys = Object.keys(options) as (keyof ClientOptions)[]
132
+ const disallowedKeys = providedKeys.filter((key) => !allowedKeys.includes(key))
133
+
134
+ if (disallowedKeys.length > 0) {
135
+ const listFormatter = new Intl.ListFormat('en', {style: 'long', type: 'conjunction'})
136
+ throw new Error(
137
+ `The client options provided contains unsupported properties: ${listFormatter.format(disallowedKeys)}. ` +
138
+ `Allowed keys are: ${listFormatter.format(allowedKeys)}.`,
139
+ )
140
+ }
141
+
142
+ const {token, clients} = state.get()
143
+ const projectId = options.projectId ?? instance.config.projectId
144
+ const dataset = options.dataset ?? instance.config.dataset
145
+ const apiHost = options.apiHost ?? instance.config.auth?.apiHost
146
+
147
+ const effectiveOptions: ClientOptions = {
148
+ ...DEFAULT_CLIENT_CONFIG,
149
+ ...((options.scope === 'global' || !projectId) && {useProjectHostname: false}),
150
+ ...(token && {token}),
151
+ ...options,
152
+ ...(projectId && {projectId}),
153
+ ...(dataset && {dataset}),
154
+ ...(apiHost && {apiHost}),
155
+ }
156
+
157
+ const key = getClientConfigKey(effectiveOptions)
158
+
159
+ if (clients[key]) return clients[key]
160
+
161
+ const client = createClient(effectiveOptions)
162
+ state.set('addClient', (prev) => ({clients: {...prev.clients, [key]: client}}))
163
+
164
+ return client
165
+ },
167
166
  )
168
167
 
169
168
  /**
170
169
  * Returns a state source for the Sanity client instance.
171
170
  *
172
171
  * This function provides a subscribable state source that emits updated client
173
- * instances whenever the client configuration changes (for example, due to
174
- * token updates). It leverages the underlying client store and memoized selector
175
- * to ensure that subscribers receive the most current client configuration.
172
+ * instances whenever relevant configurations change (such as authentication tokens).
173
+ * Use this when you need to react to client configuration changes in your application.
176
174
  *
177
175
  * @public
178
176
  */
179
- export const getClientState = createStateSourceAction(clientStore, clientSelector)
177
+ export const getClientState = bindActionGlobally(
178
+ clientStore,
179
+ createStateSourceAction(({instance}, options: ClientOptions) => getClient(instance, options)),
180
+ )
@@ -1,32 +1,57 @@
1
- import {beforeEach, describe, expect, it} from 'vitest'
1
+ import {type Controller} from '@sanity/comlink'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
2
3
 
3
- import {config} from '../../../../test/fixtures'
4
- import {createSanityInstance} from '../../../instance/sanityInstance'
5
- import {type SanityInstance} from '../../../instance/types'
6
- import {createResourceState} from '../../../resources/createResource'
7
- import {comlinkControllerStore} from '../comlinkControllerStore'
4
+ import {createSanityInstance} from '../../../store/createSanityInstance'
5
+ import {createStoreState} from '../../../store/createStoreState'
6
+ import {type ComlinkControllerState} from '../comlinkControllerStore'
8
7
  import {destroyController} from './destroyController'
9
- import {getOrCreateController} from './getOrCreateController'
10
8
 
11
9
  describe('destroyController', () => {
12
- let instance: SanityInstance
10
+ const instance = createSanityInstance({
11
+ projectId: 'test-project-id',
12
+ dataset: 'test-dataset',
13
+ })
14
+ let state: ReturnType<typeof createStoreState<ComlinkControllerState>>
15
+ let mockController: {destroy: ReturnType<typeof vi.fn>}
13
16
 
14
17
  beforeEach(() => {
15
- instance = createSanityInstance(config)
18
+ mockController = {
19
+ destroy: vi.fn(),
20
+ }
21
+
22
+ // Initialize test store state
23
+ state = createStoreState<ComlinkControllerState>({
24
+ controller: null,
25
+ controllerOrigin: null,
26
+ channels: new Map(),
27
+ })
28
+ })
29
+
30
+ afterEach(() => {
31
+ instance.dispose()
16
32
  })
17
33
 
18
34
  it('should destroy controller and clear state', () => {
19
- const state = createResourceState(comlinkControllerStore.getInitialState(instance))
20
- getOrCreateController(instance, 'https://test.sanity.dev')!
35
+ // Set up test state with a controller
36
+ state.set('setup', {
37
+ controller: mockController as unknown as Controller,
38
+ controllerOrigin: 'https://test.sanity.dev',
39
+ })
40
+
41
+ // Execute action
21
42
  destroyController({state, instance})
22
43
 
44
+ // Verify controller was destroyed and state was cleared
45
+ expect(mockController.destroy).toHaveBeenCalled()
23
46
  expect(state.get().controller).toBeNull()
24
47
  expect(state.get().channels.size).toBe(0)
25
48
  })
26
49
 
27
50
  it('should do nothing if no controller exists', () => {
28
- const state = createResourceState(comlinkControllerStore.getInitialState(instance))
29
-
51
+ // State already has null controller, so just execute action
30
52
  expect(() => destroyController({state, instance})).not.toThrow()
53
+
54
+ // State should remain unchanged
55
+ expect(state.get().controller).toBeNull()
31
56
  })
32
57
  })
@@ -1,22 +1,18 @@
1
- import {createInternalAction} from '../../../resources/createAction'
1
+ import {type StoreContext} from '../../../store/defineStore'
2
2
  import {type ComlinkControllerState} from '../comlinkControllerStore'
3
3
 
4
4
  /**
5
5
  * Calls the destroy method on the controller and resets the controller state.
6
6
  * @public
7
7
  */
8
- export const destroyController = createInternalAction<ComlinkControllerState, [], void>(
9
- ({state}) => {
10
- return () => {
11
- const {controller} = state.get()
8
+ export const destroyController = ({state}: StoreContext<ComlinkControllerState>): void => {
9
+ const {controller} = state.get()
12
10
 
13
- if (controller) {
14
- controller.destroy()
15
- state.set('destroyController', {
16
- controller: null,
17
- channels: new Map(),
18
- })
19
- }
20
- }
21
- },
22
- )
11
+ if (controller) {
12
+ controller.destroy()
13
+ state.set('destroyController', {
14
+ controller: null,
15
+ channels: new Map(),
16
+ })
17
+ }
18
+ }