@sanity/sdk 2.4.0 → 2.6.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 (57) hide show
  1. package/dist/index.d.ts +346 -110
  2. package/dist/index.js +428 -136
  3. package/dist/index.js.map +1 -1
  4. package/package.json +10 -9
  5. package/src/_exports/index.ts +15 -3
  6. package/src/auth/authStore.test.ts +13 -13
  7. package/src/auth/refreshStampedToken.test.ts +16 -16
  8. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +6 -6
  9. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -4
  10. package/src/client/clientStore.test.ts +45 -43
  11. package/src/client/clientStore.ts +23 -9
  12. package/src/comlink/controller/actions/destroyController.test.ts +2 -2
  13. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +6 -6
  14. package/src/comlink/controller/actions/getOrCreateController.test.ts +5 -5
  15. package/src/comlink/controller/actions/getOrCreateController.ts +1 -1
  16. package/src/comlink/controller/actions/releaseChannel.test.ts +3 -2
  17. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -4
  18. package/src/comlink/node/actions/getOrCreateNode.test.ts +7 -7
  19. package/src/comlink/node/actions/releaseNode.test.ts +2 -2
  20. package/src/comlink/node/comlinkNodeStore.test.ts +4 -3
  21. package/src/config/loggingConfig.ts +149 -0
  22. package/src/config/sanityConfig.ts +47 -23
  23. package/src/document/actions.ts +11 -7
  24. package/src/document/applyDocumentActions.test.ts +9 -6
  25. package/src/document/applyDocumentActions.ts +9 -49
  26. package/src/document/documentStore.test.ts +128 -115
  27. package/src/document/documentStore.ts +40 -10
  28. package/src/document/permissions.test.ts +9 -9
  29. package/src/document/permissions.ts +17 -7
  30. package/src/document/processActions.test.ts +248 -0
  31. package/src/document/processActions.ts +173 -0
  32. package/src/document/reducers.ts +13 -6
  33. package/src/presence/presenceStore.ts +13 -7
  34. package/src/preview/previewStore.test.ts +10 -2
  35. package/src/preview/previewStore.ts +2 -1
  36. package/src/preview/subscribeToStateAndFetchBatches.test.ts +8 -5
  37. package/src/preview/subscribeToStateAndFetchBatches.ts +9 -3
  38. package/src/projection/projectionStore.test.ts +18 -2
  39. package/src/projection/projectionStore.ts +2 -1
  40. package/src/projection/subscribeToStateAndFetchBatches.test.ts +6 -5
  41. package/src/projection/subscribeToStateAndFetchBatches.ts +9 -3
  42. package/src/query/queryStore.ts +3 -1
  43. package/src/releases/getPerspectiveState.ts +2 -2
  44. package/src/releases/releasesStore.ts +10 -4
  45. package/src/store/createActionBinder.test.ts +8 -6
  46. package/src/store/createActionBinder.ts +54 -28
  47. package/src/store/createSanityInstance.test.ts +85 -1
  48. package/src/store/createSanityInstance.ts +53 -4
  49. package/src/store/createStateSourceAction.test.ts +12 -11
  50. package/src/store/createStateSourceAction.ts +6 -6
  51. package/src/store/createStoreInstance.test.ts +29 -16
  52. package/src/store/createStoreInstance.ts +6 -5
  53. package/src/store/defineStore.test.ts +1 -1
  54. package/src/store/defineStore.ts +12 -7
  55. package/src/utils/logger-usage-example.md +141 -0
  56. package/src/utils/logger.test.ts +757 -0
  57. package/src/utils/logger.ts +537 -0
@@ -12,7 +12,7 @@ beforeEach(() => vi.mocked(createStoreInstance).mockClear())
12
12
 
13
13
  describe('createActionBinder', () => {
14
14
  it('should bind an action and call it with correct context and parameters, using caching', () => {
15
- const binder = createActionBinder(() => '')
15
+ const binder = createActionBinder((..._rest) => ({name: ''}))
16
16
  const storeDefinition = {
17
17
  name: 'TestStore',
18
18
  getInitialState: () => ({counter: 0}),
@@ -37,7 +37,9 @@ describe('createActionBinder', () => {
37
37
  })
38
38
 
39
39
  it('should create separate store instances for different composite keys', () => {
40
- const binder = createActionBinder(({projectId, dataset}) => `${projectId}.${dataset}`)
40
+ const binder = createActionBinder(({config: {projectId, dataset}}, ..._rest) => ({
41
+ name: `${projectId}.${dataset}`,
42
+ }))
41
43
  const storeDefinition = {
42
44
  name: 'TestStore',
43
45
  getInitialState: () => ({counter: 0}),
@@ -59,7 +61,7 @@ describe('createActionBinder', () => {
59
61
  })
60
62
 
61
63
  it('should dispose the store instance when the last instance is disposed', () => {
62
- const binder = createActionBinder(() => '')
64
+ const binder = createActionBinder((..._rest) => ({name: ''}))
63
65
  const storeDefinition = {
64
66
  name: 'TestStore',
65
67
  getInitialState: () => ({counter: 0}),
@@ -93,10 +95,10 @@ describe('bindActionByDataset', () => {
93
95
  name: 'DSStore',
94
96
  getInitialState: () => ({counter: 0}),
95
97
  }
96
- const action = vi.fn((_context, value: string) => value)
98
+ const action = vi.fn((_context, {value}: {value: string}) => value)
97
99
  const boundAction = bindActionByDataset(storeDefinition, action)
98
100
  const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
99
- const result = boundAction(instance, 'hello')
101
+ const result = boundAction(instance, {value: 'hello'})
100
102
  expect(result).toBe('hello')
101
103
  })
102
104
 
@@ -105,7 +107,7 @@ describe('bindActionByDataset', () => {
105
107
  name: 'DSStore',
106
108
  getInitialState: () => ({counter: 0}),
107
109
  }
108
- const action = vi.fn((_context) => 'fail')
110
+ const action = vi.fn((_context, _?) => 'fail')
109
111
  const boundAction = bindActionByDataset(storeDefinition, action)
110
112
  // Instance with missing dataset
111
113
  const instance = createSanityInstance({projectId: 'proj1', dataset: ''})
@@ -1,14 +1,25 @@
1
- import {type DocumentSource, type SanityConfig, SOURCE_ID} from '../config/sanityConfig'
1
+ import {
2
+ type DocumentSource,
3
+ isCanvasSource,
4
+ isDatasetSource,
5
+ isMediaLibrarySource,
6
+ } from '../config/sanityConfig'
2
7
  import {type SanityInstance} from './createSanityInstance'
3
8
  import {createStoreInstance, type StoreInstance} from './createStoreInstance'
4
9
  import {type StoreState} from './createStoreState'
5
10
  import {type StoreContext, type StoreDefinition} from './defineStore'
6
11
 
12
+ export type BoundDatasetKey = {
13
+ name: string
14
+ projectId: string
15
+ dataset: string
16
+ }
17
+
7
18
  /**
8
19
  * Defines a store action that operates on a specific state type
9
20
  */
10
- export type StoreAction<TState, TParams extends unknown[], TReturn> = (
11
- context: StoreContext<TState>,
21
+ export type StoreAction<TState, TParams extends unknown[], TReturn, TKey = unknown> = (
22
+ context: StoreContext<TState, TKey>,
12
23
  ...params: TParams
13
24
  ) => TReturn
14
25
 
@@ -43,9 +54,10 @@ export type BoundStoreAction<_TState, TParams extends unknown[], TReturn> = (
43
54
  * )
44
55
  * ```
45
56
  */
46
- export function createActionBinder<TKeyParams extends unknown[]>(
47
- keyFn: (config: SanityConfig, ...params: TKeyParams) => string,
48
- ) {
57
+ export function createActionBinder<
58
+ TKey extends {name: string},
59
+ TKeyParams extends unknown[] = unknown[],
60
+ >(keyFn: (instance: SanityInstance, ...params: TKeyParams) => TKey) {
49
61
  const instanceRegistry = new Map<string, Set<string>>()
50
62
  const storeRegistry = new Map<string, StoreInstance<unknown>>()
51
63
 
@@ -57,12 +69,12 @@ export function createActionBinder<TKeyParams extends unknown[]>(
57
69
  * @returns A function that executes the action with a Sanity instance
58
70
  */
59
71
  return function bindAction<TState, TParams extends TKeyParams, TReturn>(
60
- storeDefinition: StoreDefinition<TState>,
61
- action: StoreAction<TState, TParams, TReturn>,
72
+ storeDefinition: StoreDefinition<TState, TKey>,
73
+ action: StoreAction<TState, TParams, TReturn, TKey>,
62
74
  ): BoundStoreAction<TState, TParams, TReturn> {
63
75
  return function boundAction(instance: SanityInstance, ...params: TParams) {
64
- const keySuffix = keyFn(instance.config, ...params)
65
- const compositeKey = storeDefinition.name + (keySuffix ? `:${keySuffix}` : '')
76
+ const key = keyFn(instance, ...params)
77
+ const compositeKey = storeDefinition.name + (key.name ? `:${key.name}` : '')
66
78
 
67
79
  // Get or create instance set for this composite key
68
80
  let instances = instanceRegistry.get(compositeKey)
@@ -89,12 +101,12 @@ export function createActionBinder<TKeyParams extends unknown[]>(
89
101
  // Get or create store instance
90
102
  let storeInstance = storeRegistry.get(compositeKey)
91
103
  if (!storeInstance) {
92
- storeInstance = createStoreInstance(instance, storeDefinition)
104
+ storeInstance = createStoreInstance(instance, key, storeDefinition)
93
105
  storeRegistry.set(compositeKey, storeInstance)
94
106
  }
95
107
 
96
108
  // Execute action with store context
97
- return action({instance, state: storeInstance.state as StoreState<TState>}, ...params)
109
+ return action({instance, state: storeInstance.state as StoreState<TState>, key}, ...params)
98
110
  }
99
111
  }
100
112
  }
@@ -130,31 +142,45 @@ export function createActionBinder<TKeyParams extends unknown[]>(
130
142
  * fetchDocument(sanityInstance, 'doc123')
131
143
  * ```
132
144
  */
133
- export const bindActionByDataset = createActionBinder<unknown[]>(({projectId, dataset}) => {
145
+ export const bindActionByDataset = createActionBinder<
146
+ BoundDatasetKey,
147
+ [(object & {projectId?: string; dataset?: string})?, ...unknown[]]
148
+ >((instance, options) => {
149
+ const projectId = options?.projectId ?? instance.config.projectId
150
+ const dataset = options?.dataset ?? instance.config.dataset
134
151
  if (!projectId || !dataset) {
135
152
  throw new Error('This API requires a project ID and dataset configured.')
136
153
  }
137
- return `${projectId}.${dataset}`
154
+ return {name: `${projectId}.${dataset}`, projectId, dataset}
138
155
  })
139
156
 
140
157
  /**
141
158
  * Binds an action to a store that's scoped to a specific document source.
142
159
  **/
143
- export const bindActionBySource = createActionBinder<[{source?: DocumentSource}, ...unknown[]]>(
144
- ({projectId, dataset}, {source}) => {
145
- if (source) {
146
- const id = source[SOURCE_ID]
147
- if (!id) throw new Error('Invalid source (missing ID information)')
148
- if (Array.isArray(id)) return id.join(':')
149
- return `${id.projectId}.${id.dataset}`
160
+ export const bindActionBySource = createActionBinder<
161
+ {name: string},
162
+ [{source?: DocumentSource}, ...unknown[]]
163
+ >((instance, {source}) => {
164
+ if (source) {
165
+ let id: string | undefined
166
+ if (isDatasetSource(source)) {
167
+ id = `${source.projectId}.${source.dataset}`
168
+ } else if (isMediaLibrarySource(source)) {
169
+ id = `media-library:${source.mediaLibraryId}`
170
+ } else if (isCanvasSource(source)) {
171
+ id = `canvas:${source.canvasId}`
150
172
  }
151
173
 
152
- if (!projectId || !dataset) {
153
- throw new Error('This API requires a project ID and dataset configured.')
154
- }
155
- return `${projectId}.${dataset}`
156
- },
157
- )
174
+ if (!id) throw new Error(`Received invalid source: ${JSON.stringify(source)}`)
175
+ return {name: id}
176
+ }
177
+ const {projectId, dataset} = instance.config
178
+
179
+ if (!projectId || !dataset) {
180
+ throw new Error('This API requires a project ID and dataset configured.')
181
+ }
182
+ return {name: `${projectId}.${dataset}`}
183
+ })
158
184
 
159
185
  /**
160
186
  * Binds an action to a global store that's shared across all Sanity instances
@@ -194,4 +220,4 @@ export const bindActionBySource = createActionBinder<[{source?: DocumentSource},
194
220
  * getCurrentUser(sanityInstance)
195
221
  * ```
196
222
  */
197
- export const bindActionGlobally = createActionBinder<unknown[]>(() => 'global')
223
+ export const bindActionGlobally = createActionBinder((..._rest) => ({name: 'global'}))
@@ -1,5 +1,6 @@
1
- import {describe, expect, it, vi} from 'vitest'
1
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
2
2
 
3
+ import {configureLogging, type LogHandler, resetLogging} from '../utils/logger'
3
4
  import {createSanityInstance} from './createSanityInstance'
4
5
 
5
6
  describe('createSanityInstance', () => {
@@ -81,4 +82,87 @@ describe('createSanityInstance', () => {
81
82
  const child = parent.createChild({auth: {token: 'my-token'}})
82
83
  expect(child.config.auth).toEqual({apiHost: 'api.sanity.work', token: 'my-token'})
83
84
  })
85
+
86
+ describe('logging', () => {
87
+ const mockHandler: LogHandler = {
88
+ error: vi.fn(),
89
+ warn: vi.fn(),
90
+ info: vi.fn(),
91
+ debug: vi.fn(),
92
+ trace: vi.fn(),
93
+ }
94
+
95
+ beforeEach(() => {
96
+ vi.clearAllMocks()
97
+ configureLogging({
98
+ level: 'debug',
99
+ namespaces: ['sdk'],
100
+ handler: mockHandler,
101
+ })
102
+ })
103
+
104
+ afterEach(() => {
105
+ resetLogging()
106
+ })
107
+
108
+ it('should log instance creation at info level', () => {
109
+ createSanityInstance({projectId: 'test-proj', dataset: 'test-ds'})
110
+
111
+ expect(mockHandler.info).toHaveBeenCalledWith(
112
+ expect.stringContaining('[INFO] [sdk]'),
113
+ expect.objectContaining({
114
+ hasProjectId: true,
115
+ hasDataset: true,
116
+ }),
117
+ )
118
+ })
119
+
120
+ it('should log configuration details at debug level', () => {
121
+ createSanityInstance({projectId: 'test-proj', dataset: 'test-ds'})
122
+
123
+ expect(mockHandler.debug).toHaveBeenCalledWith(
124
+ expect.stringContaining('[DEBUG] [sdk]'),
125
+ expect.objectContaining({
126
+ projectId: 'test-proj',
127
+ dataset: 'test-ds',
128
+ }),
129
+ )
130
+ })
131
+
132
+ it('should log instance disposal', () => {
133
+ const instance = createSanityInstance({projectId: 'test-proj'})
134
+ vi.clearAllMocks() // Clear creation logs
135
+
136
+ instance.dispose()
137
+
138
+ expect(mockHandler.info).toHaveBeenCalledWith(
139
+ expect.stringContaining('Instance disposed'),
140
+ expect.anything(),
141
+ )
142
+ })
143
+
144
+ it('should log child instance creation at debug level', () => {
145
+ const parent = createSanityInstance({projectId: 'parent-proj'})
146
+ vi.clearAllMocks() // Clear parent creation logs
147
+
148
+ parent.createChild({dataset: 'child-ds'})
149
+
150
+ expect(mockHandler.debug).toHaveBeenCalledWith(
151
+ expect.stringContaining('Creating child instance'),
152
+ expect.objectContaining({
153
+ overridingDataset: true,
154
+ }),
155
+ )
156
+ })
157
+
158
+ it('should include instance context in logs', () => {
159
+ createSanityInstance({projectId: 'my-project', dataset: 'my-dataset'})
160
+
161
+ // Check that logs include the instance context (project and dataset)
162
+ expect(mockHandler.info).toHaveBeenCalledWith(
163
+ expect.stringMatching(/\[project:my-project\].*\[dataset:my-dataset\]/),
164
+ expect.anything(),
165
+ )
166
+ })
167
+ })
84
168
  })
@@ -2,6 +2,7 @@ import {pick} from 'lodash-es'
2
2
 
3
3
  import {type SanityConfig} from '../config/sanityConfig'
4
4
  import {insecureRandomId} from '../utils/ids'
5
+ import {createLogger, type InstanceContext} from '../utils/logger'
5
6
 
6
7
  /**
7
8
  * Represents a Sanity.io resource instance with its own configuration and lifecycle
@@ -76,15 +77,51 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
76
77
  const disposeListeners = new Map<string, () => void>()
77
78
  const disposed = {current: false}
78
79
 
80
+ // Create instance context for logging
81
+ const instanceContext: InstanceContext = {
82
+ instanceId,
83
+ projectId: config.projectId,
84
+ dataset: config.dataset,
85
+ }
86
+
87
+ // Create logger with instance context
88
+ const logger = createLogger('sdk', {instanceContext})
89
+
90
+ // Log instance creation
91
+ logger.info('Sanity instance created', {
92
+ hasProjectId: !!config.projectId,
93
+ hasDataset: !!config.dataset,
94
+ hasAuth: !!config.auth,
95
+ hasPerspective: !!config.perspective,
96
+ })
97
+
98
+ // Log configuration details at debug level
99
+ logger.debug('Instance configuration', {
100
+ projectId: config.projectId,
101
+ dataset: config.dataset,
102
+ perspective: config.perspective,
103
+ studioMode: config.studioMode?.enabled,
104
+ hasAuthProviders: !!config.auth?.providers,
105
+ hasAuthToken: !!config.auth?.token,
106
+ })
107
+
79
108
  const instance: SanityInstance = {
80
109
  instanceId,
81
110
  config,
82
111
  isDisposed: () => disposed.current,
83
112
  dispose: () => {
84
- if (disposed.current) return
113
+ if (disposed.current) {
114
+ logger.trace('Dispose called on already disposed instance', {internal: true})
115
+ return
116
+ }
117
+ logger.trace('Disposing instance', {
118
+ internal: true,
119
+ listenerCount: disposeListeners.size,
120
+ })
85
121
  disposed.current = true
86
122
  disposeListeners.forEach((listener) => listener())
87
123
  disposeListeners.clear()
124
+ logger.info('Instance disposed')
88
125
  },
89
126
  onDispose: (cb) => {
90
127
  const listenerId = insecureRandomId()
@@ -94,8 +131,14 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
94
131
  }
95
132
  },
96
133
  getParent: () => undefined,
97
- createChild: (next) =>
98
- Object.assign(
134
+ createChild: (next) => {
135
+ logger.debug('Creating child instance', {
136
+ parentInstanceId: instanceId.slice(0, 8),
137
+ overridingProjectId: !!next.projectId,
138
+ overridingDataset: !!next.dataset,
139
+ overridingAuth: !!next.auth,
140
+ })
141
+ const child = Object.assign(
99
142
  createSanityInstance({
100
143
  ...config,
101
144
  ...next,
@@ -104,7 +147,13 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
104
147
  : config.auth && next.auth && {auth: {...config.auth, ...next.auth}}),
105
148
  }),
106
149
  {getParent: () => instance},
107
- ),
150
+ )
151
+ logger.trace('Child instance created', {
152
+ internal: true,
153
+ childInstanceId: child.instanceId.slice(0, 8),
154
+ })
155
+ return child
156
+ },
108
157
  match: (targetConfig) => {
109
158
  if (
110
159
  Object.entries(pick(targetConfig, 'auth', 'projectId', 'dataset')).every(
@@ -21,7 +21,7 @@ describe('createStateSourceAction', () => {
21
21
  it('should create a source that provides current state through getCurrent', () => {
22
22
  const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
23
23
  const action = createStateSourceAction(selector)
24
- const source = action({state, instance})
24
+ const source = action({state, instance, key: null})
25
25
 
26
26
  expect(source.getCurrent()).toBe(0)
27
27
  state.set('test', {count: 5})
@@ -33,7 +33,7 @@ describe('createStateSourceAction', () => {
33
33
  const source = createStateSourceAction({
34
34
  selector: ({state: s}: SelectorContext<CountStoreState>) => s.count,
35
35
  isEqual: (a, b) => a === b,
36
- })({state, instance})
36
+ })({state, instance, key: null})
37
37
 
38
38
  const unsubscribe = source.subscribe(onStoreChanged)
39
39
 
@@ -53,11 +53,11 @@ describe('createStateSourceAction', () => {
53
53
  const source = createStateSourceAction({
54
54
  selector: ({state: s}: SelectorContext<CountStoreState>) => s.items,
55
55
  onSubscribe,
56
- })({state, instance})
56
+ })({state, instance, key: null})
57
57
 
58
58
  const unsubscribe = source.subscribe()
59
59
  expect(onSubscribe).toHaveBeenCalledWith(
60
- expect.objectContaining({state, instance}),
60
+ expect.objectContaining({state, instance, key: null}),
61
61
  // No params in this case
62
62
  )
63
63
 
@@ -68,7 +68,7 @@ describe('createStateSourceAction', () => {
68
68
  const action = createStateSourceAction({
69
69
  selector: ({state: s}: SelectorContext<CountStoreState>, index: number) => s.items[index],
70
70
  })
71
- const source = action({state, instance}, 0)
71
+ const source = action({state, instance, key: null}, 0)
72
72
 
73
73
  state.set('add', {items: ['first']})
74
74
  expect(source.getCurrent()).toBe('first')
@@ -80,7 +80,7 @@ describe('createStateSourceAction', () => {
80
80
  selector: () => {
81
81
  throw error
82
82
  },
83
- })({state, instance})
83
+ })({state, instance, key: null})
84
84
 
85
85
  const errorHandler = vi.fn()
86
86
  source.observable.subscribe({error: errorHandler})
@@ -94,7 +94,7 @@ describe('createStateSourceAction', () => {
94
94
  const source = createStateSourceAction({
95
95
  selector: ({state: s}: SelectorContext<CountStoreState>) => s.items.map((i) => i.length),
96
96
  isEqual,
97
- })({state, instance})
97
+ })({state, instance, key: null})
98
98
 
99
99
  const onChange = vi.fn()
100
100
  source.subscribe(onChange)
@@ -112,7 +112,7 @@ describe('createStateSourceAction', () => {
112
112
  const source = createStateSourceAction({
113
113
  selector: ({state: s}: SelectorContext<CountStoreState>) => s.count,
114
114
  onSubscribe: () => cleanup,
115
- })({state, instance})
115
+ })({state, instance, key: null})
116
116
 
117
117
  const unsubscribe = source.subscribe()
118
118
  unsubscribe()
@@ -125,6 +125,7 @@ describe('createStateSourceAction', () => {
125
125
  )({
126
126
  state,
127
127
  instance,
128
+ key: null,
128
129
  })
129
130
 
130
131
  const subscriber1 = vi.fn()
@@ -144,7 +145,7 @@ describe('createStateSourceAction', () => {
144
145
 
145
146
  it('should cache selector context per state object', () => {
146
147
  const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
147
- const source = createStateSourceAction(selector)({state, instance})
148
+ const source = createStateSourceAction(selector)({state, instance, key: null})
148
149
 
149
150
  // Initial call creates context
150
151
  expect(source.getCurrent()).toBe(0)
@@ -181,10 +182,10 @@ describe('createStateSourceAction', () => {
181
182
  const secondInstance = createSanityInstance({projectId: 'test2', dataset: 'test2'})
182
183
  const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
183
184
 
184
- const source1 = createStateSourceAction(selector)({state, instance})
185
+ const source1 = createStateSourceAction(selector)({state, instance, key: null})
185
186
  source1.getCurrent()
186
187
 
187
- const source2 = createStateSourceAction(selector)({state, instance: secondInstance})
188
+ const source2 = createStateSourceAction(selector)({state, instance: secondInstance, key: null})
188
189
  source2.getCurrent()
189
190
 
190
191
  const context1 = selector.mock.calls[0][0]
@@ -89,7 +89,7 @@ export type Selector<TState, TParams extends unknown[], TReturn> = (
89
89
  /**
90
90
  * Configuration options for creating a state source action
91
91
  */
92
- interface StateSourceOptions<TState, TParams extends unknown[], TReturn> {
92
+ interface StateSourceOptions<TState, TParams extends unknown[], TReturn, TKey> {
93
93
  /**
94
94
  * Selector function that derives the desired value from store state
95
95
  *
@@ -106,7 +106,7 @@ interface StateSourceOptions<TState, TParams extends unknown[], TReturn> {
106
106
  * @param params - Action parameters provided during invocation
107
107
  * @returns Optional cleanup function called when subscription ends
108
108
  */
109
- onSubscribe?: (context: StoreContext<TState>, ...params: TParams) => void | (() => void)
109
+ onSubscribe?: (context: StoreContext<TState, TKey>, ...params: TParams) => void | (() => void)
110
110
 
111
111
  /**
112
112
  * Equality function to prevent unnecessary updates
@@ -168,9 +168,9 @@ interface StateSourceOptions<TState, TParams extends unknown[], TReturn> {
168
168
  * })
169
169
  * ```
170
170
  */
171
- export function createStateSourceAction<TState, TParams extends unknown[], TReturn>(
172
- options: Selector<TState, TParams, TReturn> | StateSourceOptions<TState, TParams, TReturn>,
173
- ): StoreAction<TState, TParams, StateSource<TReturn>> {
171
+ export function createStateSourceAction<TState, TParams extends unknown[], TReturn, TKey = unknown>(
172
+ options: Selector<TState, TParams, TReturn> | StateSourceOptions<TState, TParams, TReturn, TKey>,
173
+ ): StoreAction<TState, TParams, StateSource<TReturn>, TKey> {
174
174
  const selector = typeof options === 'function' ? options : options.selector
175
175
  const subscribeHandler = options && 'onSubscribe' in options ? options.onSubscribe : undefined
176
176
  const isEqual = options && 'isEqual' in options ? (options.isEqual ?? Object.is) : Object.is
@@ -184,7 +184,7 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
184
184
  * @param context - Store context providing access to state and instance
185
185
  * @param params - Parameters provided when invoking the bound action
186
186
  */
187
- function stateSourceAction(context: StoreContext<TState>, ...params: TParams) {
187
+ function stateSourceAction(context: StoreContext<TState, TKey>, ...params: TParams) {
188
188
  const {state, instance} = context
189
189
 
190
190
  const getCurrent = () => {
@@ -24,36 +24,45 @@ describe('createStoreInstance', () => {
24
24
  }
25
25
 
26
26
  it('should create store instance with initial state', () => {
27
- const store = createStoreInstance(instance, storeDef)
27
+ const store = createStoreInstance(instance, {name: 'store'}, storeDef)
28
28
  expect(store.state).toBeDefined()
29
29
  })
30
30
 
31
31
  it('should call getInitialState with Sanity instance', () => {
32
32
  const getInitialState = vi.fn(() => ({count: 0}))
33
- createStoreInstance(instance, {...storeDef, getInitialState})
34
- expect(getInitialState).toHaveBeenCalledWith(instance)
33
+ createStoreInstance(instance, {name: 'store'}, {...storeDef, getInitialState})
34
+ expect(getInitialState).toHaveBeenCalledWith(instance, {name: 'store'})
35
35
  })
36
36
 
37
37
  it('should call initialize function with context', () => {
38
38
  const initialize = vi.fn()
39
39
 
40
- const store = createStoreInstance(instance, {
41
- ...storeDef,
42
- initialize,
43
- })
40
+ const store = createStoreInstance(
41
+ instance,
42
+ {name: 'store'},
43
+ {
44
+ ...storeDef,
45
+ initialize,
46
+ },
47
+ )
44
48
  expect(initialize).toHaveBeenCalledWith({
45
49
  state: store.state,
46
50
  instance,
51
+ key: {name: 'store'},
47
52
  })
48
53
  })
49
54
 
50
55
  it('should handle store disposal with cleanup function', () => {
51
56
  const disposeMock = vi.fn()
52
57
 
53
- const store = createStoreInstance(instance, {
54
- ...storeDef,
55
- initialize: () => disposeMock,
56
- })
58
+ const store = createStoreInstance(
59
+ instance,
60
+ {name: 'store'},
61
+ {
62
+ ...storeDef,
63
+ initialize: () => disposeMock,
64
+ },
65
+ )
57
66
  store.dispose()
58
67
 
59
68
  expect(disposeMock).toHaveBeenCalledTimes(1)
@@ -61,7 +70,7 @@ describe('createStoreInstance', () => {
61
70
  })
62
71
 
63
72
  it('should handle disposal without initialize function', () => {
64
- const store = createStoreInstance(instance, storeDef)
73
+ const store = createStoreInstance(instance, {name: 'store'}, storeDef)
65
74
  store.dispose()
66
75
  expect(store.isDisposed()).toBe(true)
67
76
  })
@@ -69,10 +78,14 @@ describe('createStoreInstance', () => {
69
78
  it('should prevent multiple disposals', () => {
70
79
  const disposeMock = vi.fn()
71
80
 
72
- const store = createStoreInstance(instance, {
73
- ...storeDef,
74
- initialize: () => disposeMock,
75
- })
81
+ const store = createStoreInstance(
82
+ instance,
83
+ {name: 'store'},
84
+ {
85
+ ...storeDef,
86
+ initialize: () => disposeMock,
87
+ },
88
+ )
76
89
  store.dispose()
77
90
  store.dispose()
78
91
 
@@ -57,15 +57,16 @@ export interface StoreInstance<TState> {
57
57
  * instance.dispose()
58
58
  * ```
59
59
  */
60
- export function createStoreInstance<TState>(
60
+ export function createStoreInstance<TState, TKey extends {name: string}>(
61
61
  instance: SanityInstance,
62
- {name, getInitialState, initialize}: StoreDefinition<TState>,
62
+ key: TKey,
63
+ {name, getInitialState, initialize}: StoreDefinition<TState, TKey>,
63
64
  ): StoreInstance<TState> {
64
- const state = createStoreState(getInitialState(instance), {
65
+ const state = createStoreState(getInitialState(instance, key), {
65
66
  enabled: !!getEnv('DEV'),
66
- name: `${name}-${instance.config.projectId}.${instance.config.dataset}`,
67
+ name: `${name}-${key.name}`,
67
68
  })
68
- const dispose = initialize?.({state, instance})
69
+ const dispose = initialize?.({state, instance, key})
69
70
  const disposed = {current: false}
70
71
 
71
72
  return {
@@ -13,6 +13,6 @@ describe('defineStore', () => {
13
13
  const result = defineStore(storeDef)
14
14
  expect(result).toBe(storeDef)
15
15
  expect(result.name).toBe('TestStore')
16
- expect(result.getInitialState({} as SanityInstance)).toBe(42)
16
+ expect(result.getInitialState({} as SanityInstance, null)).toBe(42)
17
17
  })
18
18
  })