@sanity/sdk 0.0.0-chore-react-18-compat.1 → 0.0.0-chore-react-18-compat.3

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 (134) hide show
  1. package/dist/index.d.ts +441 -322
  2. package/dist/index.js +1685 -1481
  3. package/dist/index.js.map +1 -1
  4. package/package.json +13 -15
  5. package/src/_exports/index.ts +32 -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 +197 -91
  13. package/src/auth/refreshStampedToken.ts +170 -59
  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 -238
  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 +188 -72
  70. package/src/projection/getProjectionState.ts +92 -62
  71. package/src/projection/projectionQuery.test.ts +114 -12
  72. package/src/projection/projectionQuery.ts +75 -32
  73. package/src/projection/projectionStore.test.ts +13 -51
  74. package/src/projection/projectionStore.ts +6 -43
  75. package/src/projection/resolveProjection.test.ts +32 -127
  76. package/src/projection/resolveProjection.ts +16 -28
  77. package/src/projection/subscribeToStateAndFetchBatches.test.ts +203 -116
  78. package/src/projection/subscribeToStateAndFetchBatches.ts +140 -85
  79. package/src/projection/types.ts +50 -0
  80. package/src/projection/util.ts +3 -1
  81. package/src/projects/projects.test.ts +13 -4
  82. package/src/projects/projects.ts +6 -1
  83. package/src/query/queryStore.test.ts +10 -47
  84. package/src/query/queryStore.ts +151 -133
  85. package/src/query/queryStoreConstants.ts +2 -0
  86. package/src/store/createActionBinder.test.ts +153 -0
  87. package/src/store/createActionBinder.ts +176 -0
  88. package/src/store/createSanityInstance.test.ts +84 -0
  89. package/src/store/createSanityInstance.ts +124 -0
  90. package/src/store/createStateSourceAction.test.ts +196 -0
  91. package/src/store/createStateSourceAction.ts +260 -0
  92. package/src/store/createStoreInstance.test.ts +81 -0
  93. package/src/store/createStoreInstance.ts +80 -0
  94. package/src/store/createStoreState.test.ts +85 -0
  95. package/src/store/createStoreState.ts +92 -0
  96. package/src/store/defineStore.test.ts +18 -0
  97. package/src/store/defineStore.ts +81 -0
  98. package/src/users/reducers.test.ts +318 -0
  99. package/src/users/reducers.ts +88 -0
  100. package/src/users/types.ts +46 -4
  101. package/src/users/usersConstants.ts +4 -0
  102. package/src/users/usersStore.test.ts +350 -223
  103. package/src/users/usersStore.ts +285 -149
  104. package/src/utils/createFetcherStore.test.ts +6 -7
  105. package/src/utils/createFetcherStore.ts +150 -153
  106. package/src/utils/createGroqSearchFilter.test.ts +75 -0
  107. package/src/utils/createGroqSearchFilter.ts +85 -0
  108. package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
  109. package/dist/index.cjs +0 -4888
  110. package/dist/index.cjs.map +0 -1
  111. package/dist/index.d.cts +0 -2121
  112. package/src/auth/fetchLoginUrls.test.ts +0 -163
  113. package/src/auth/fetchLoginUrls.ts +0 -74
  114. package/src/common/createLiveEventSubscriber.test.ts +0 -121
  115. package/src/common/createLiveEventSubscriber.ts +0 -55
  116. package/src/common/types.ts +0 -4
  117. package/src/instance/identity.test.ts +0 -46
  118. package/src/instance/identity.ts +0 -29
  119. package/src/instance/sanityInstance.test.ts +0 -77
  120. package/src/instance/sanityInstance.ts +0 -57
  121. package/src/instance/types.ts +0 -37
  122. package/src/preview/getPreviewProjection.ts +0 -45
  123. package/src/resources/README.md +0 -370
  124. package/src/resources/createAction.test.ts +0 -101
  125. package/src/resources/createAction.ts +0 -44
  126. package/src/resources/createResource.test.ts +0 -112
  127. package/src/resources/createResource.ts +0 -102
  128. package/src/resources/createStateSourceAction.test.ts +0 -114
  129. package/src/resources/createStateSourceAction.ts +0 -83
  130. package/src/resources/createStore.test.ts +0 -67
  131. package/src/resources/createStore.ts +0 -46
  132. package/src/store/createStore.test.ts +0 -108
  133. package/src/store/createStore.ts +0 -106
  134. /package/src/{common/util.ts → utils/hashString.ts} +0 -0
@@ -1,123 +1,239 @@
1
- import {describe, it, type Mock} from 'vitest'
2
-
3
- import {createSanityInstance} from '../instance/sanityInstance'
4
- import {
5
- createResourceState,
6
- getOrCreateResource,
7
- type ResourceState,
8
- } from '../resources/createResource'
1
+ import {NEVER} from 'rxjs'
2
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
5
+ import {type StoreState} from '../store/createStoreState'
6
+ import {hashString} from '../utils/hashString'
9
7
  import {insecureRandomId} from '../utils/ids'
10
8
  import {getProjectionState} from './getProjectionState'
11
- import {projectionStore, type ProjectionStoreState} from './projectionStore'
12
- import {STABLE_EMPTY_PROJECTION} from './util'
9
+ import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
10
+ import {type ProjectionStoreState} from './types'
11
+ import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION} from './util'
12
+ // mocking subscription counts is a little tricky.
13
+ // all test ids in this file start at 2 because the first call to onSubscribe
14
+ // happens within createStateSourceAction
15
+ let mockIdCounter = 0
13
16
 
14
17
  vi.mock('../utils/ids', async (importOriginal) => {
15
18
  const util = await importOriginal<typeof import('../utils/ids')>()
16
- return {...util, insecureRandomId: vi.fn(util.insecureRandomId)}
19
+ // Mock implementation uses the external counter
20
+ return {
21
+ ...util,
22
+ insecureRandomId: vi.fn(() => {
23
+ const id = `testSubId_${++mockIdCounter}`
24
+ return id
25
+ }),
26
+ }
17
27
  })
18
28
 
19
- vi.mock('../resources/createResource', async (importOriginal) => {
20
- const original = await importOriginal<typeof import('../resources/createResource')>()
21
- return {...original, getOrCreateResource: vi.fn()}
22
- })
29
+ vi.mock('./subscribeToStateAndFetchBatches.ts')
23
30
 
24
31
  describe('getProjectionState', () => {
25
- const instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
26
- const document = {_id: 'exampleId', _type: 'exampleType'}
27
- const projection = '{exampleProjection}'
28
- const initialState: ProjectionStoreState = {
29
- values: {},
30
- documentProjections: {},
31
- subscriptions: {},
32
- syncTags: {},
33
- lastLiveEventId: null,
34
- }
35
- let state: ResourceState<ProjectionStoreState>
32
+ let instance: SanityInstance
33
+ const docHandle = {documentId: 'exampleId', documentType: 'exampleType'}
34
+ const projection1 = '{exampleProjection1}'
35
+ const hash1 = hashString(projection1)
36
+ const projection2 = '{exampleProjection2}'
37
+ const hash2 = hashString(projection2)
38
+
39
+ let state: StoreState<ProjectionStoreState & {extra?: unknown}>
36
40
 
37
41
  beforeEach(() => {
38
- state = createResourceState(initialState)
42
+ mockIdCounter = 0
43
+ vi.mocked(insecureRandomId).mockClear()
44
+
45
+ // Capture state
46
+ vi.mocked(subscribeToStateAndFetchBatches).mockImplementation((context) => {
47
+ state = context.state
48
+ return NEVER.subscribe()
49
+ })
50
+
51
+ instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
52
+ vi.useFakeTimers() // Enable fake timers for each test
53
+ })
54
+
55
+ afterEach(() => {
56
+ instance.dispose()
57
+ vi.useRealTimers() // Restore real timers
39
58
  })
40
59
 
41
- it('returns a state source that emits when the projection value changes', () => {
42
- const projectionState = getProjectionState({state, instance}, {document, projection})
43
- expect(projectionState.getCurrent()).toBe(STABLE_EMPTY_PROJECTION)
60
+ it('returns a state source that emits when the specific projection value changes', () => {
61
+ const projectionState = getProjectionState(instance, {projection: projection1, ...docHandle})
62
+ expect(projectionState.getCurrent()).toEqual(STABLE_EMPTY_PROJECTION)
44
63
 
45
64
  const subscriber = vi.fn()
46
- projectionState.subscribe(subscriber)
65
+ const unsubscribe = projectionState.subscribe(subscriber)
47
66
 
48
- // emit unrelated state changes
49
- state.set('updateLastLiveEventId', {lastLiveEventId: 'newLastLiveEventId'})
50
- expect(subscriber).toHaveBeenCalledTimes(0)
67
+ // 1. Update the specific projection
68
+ state.set('update_doc1_proj1', (prev: ProjectionStoreState) => ({
69
+ values: {
70
+ ...prev.values,
71
+ [docHandle.documentId]: {
72
+ ...prev.values[docHandle.documentId],
73
+ [hash1]: {data: {name: 'Update 1'}, isPending: false},
74
+ },
75
+ },
76
+ }))
77
+ expect(subscriber).toHaveBeenCalledTimes(1)
78
+ expect(projectionState.getCurrent()).toEqual({data: {name: 'Update 1'}, isPending: false})
51
79
 
52
- state.set('relatedChange', (prev) => ({
53
- values: {...prev.values, exampleId: {data: {field: 'Changed!'}, isPending: false}},
80
+ // 2. Update a different projection for the same document
81
+ state.set('update_doc1_proj2', (prev: ProjectionStoreState) => ({
82
+ values: {
83
+ ...prev.values,
84
+ [docHandle.documentId]: {
85
+ ...prev.values[docHandle.documentId],
86
+ [hash2]: {data: {_type: 'type1'}, isPending: false},
87
+ },
88
+ },
54
89
  }))
90
+ // Should NOT trigger the subscriber for projection1
55
91
  expect(subscriber).toHaveBeenCalledTimes(1)
92
+ expect(projectionState.getCurrent()).toEqual({data: {name: 'Update 1'}, isPending: false})
56
93
 
57
- state.set('unrelatedChange', (prev) => ({
94
+ // 3. Update a different document
95
+ state.set('update_doc2', (prev: ProjectionStoreState) => ({
58
96
  values: {
59
97
  ...prev.values,
60
- unrelatedId: {data: {field: 'Unrelated Document'}, isPending: false},
98
+ doc2: {
99
+ [hash1]: {data: {name: 'Other Doc'}, isPending: false},
100
+ },
61
101
  },
62
102
  }))
103
+ // Should NOT trigger the subscriber for doc1/projection1
63
104
  expect(subscriber).toHaveBeenCalledTimes(1)
64
105
 
65
- state.set('relatedChange', (prev) => ({
66
- values: {...prev.values, exampleId: {data: {field: 'Changed again!'}, isPending: false}},
106
+ // 4. Update the specific projection again
107
+ state.set('update_doc1_proj1_again', (prev: ProjectionStoreState) => ({
108
+ values: {
109
+ ...prev.values,
110
+ [docHandle.documentId]: {
111
+ ...prev.values[docHandle.documentId],
112
+ [hash1]: {data: {name: 'Update 2'}, isPending: false},
113
+ },
114
+ },
67
115
  }))
68
116
  expect(subscriber).toHaveBeenCalledTimes(2)
117
+ expect(projectionState.getCurrent()).toEqual({data: {name: 'Update 2'}, isPending: false})
118
+
119
+ unsubscribe()
69
120
  })
70
121
 
71
- it('adds a subscription ID and projection to the state on subscription', () => {
72
- const projectionState = getProjectionState({state, instance}, {document, projection})
122
+ it('adds a subscription ID and projection to the correct hash on subscription and cleans up', () => {
123
+ const projectionState1 = getProjectionState(instance, {projection: projection1, ...docHandle})
124
+ const projectionState2 = getProjectionState(instance, {projection: projection2, ...docHandle})
73
125
 
74
126
  expect(state.get().subscriptions).toEqual({})
75
- vi.mocked(insecureRandomId)
76
- .mockImplementationOnce(() => 'pseudoRandomId1')
77
- .mockImplementationOnce(() => 'pseudoRandomId2')
127
+ expect(state.get().documentProjections).toEqual({})
78
128
 
79
- const unsubscribe1 = projectionState.subscribe(vi.fn())
80
- const unsubscribe2 = projectionState.subscribe(vi.fn())
129
+ const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID 3
130
+ expect(state.get().subscriptions).toEqual({
131
+ [docHandle.documentId]: {[hash1]: {testSubId_2: true}},
132
+ })
133
+ expect(state.get().documentProjections).toEqual({
134
+ [docHandle.documentId]: {[hash1]: projection1},
135
+ })
81
136
 
137
+ const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID 4
82
138
  expect(state.get().subscriptions).toEqual({
83
- exampleId: {pseudoRandomId1: true, pseudoRandomId2: true},
139
+ [docHandle.documentId]: {
140
+ [hash1]: {testSubId_2: true},
141
+ [hash2]: {testSubId_3: true},
142
+ },
84
143
  })
85
144
  expect(state.get().documentProjections).toEqual({
86
- exampleId: projection,
145
+ [docHandle.documentId]: {
146
+ [hash1]: projection1,
147
+ [hash2]: projection2,
148
+ },
87
149
  })
88
150
 
89
- unsubscribe2()
151
+ const unsubscribe3 = projectionState1.subscribe(vi.fn()) // Should use ID 5
90
152
  expect(state.get().subscriptions).toEqual({
91
- exampleId: {pseudoRandomId1: true},
153
+ [docHandle.documentId]: {
154
+ [hash1]: {testSubId_2: true, testSubId_4: true},
155
+ [hash2]: {testSubId_3: true},
156
+ },
92
157
  })
93
158
 
94
- unsubscribe1()
95
- expect(state.get().subscriptions).toEqual({})
96
- })
159
+ // projections state should remain the same, even with multiple subscribers
160
+ expect(state.get().documentProjections).toEqual({
161
+ [docHandle.documentId]: {
162
+ [hash1]: projection1,
163
+ [hash2]: projection2,
164
+ },
165
+ })
97
166
 
98
- it('resets to pending false on unsubscribe if the subscription is the last one', () => {
99
- state.set('presetValueToPending', (prev) => ({
100
- values: {...prev.values, [document._id]: {data: {field: 'Foo'}, isPending: true}},
101
- }))
167
+ // --- Test Unsubscribe ---
168
+ unsubscribe1() // Unsubscribes ID 3
169
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({
170
+ testSubId_2: true,
171
+ testSubId_4: true,
172
+ })
173
+ vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
174
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_4: true})
175
+ expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toEqual(projection1)
176
+
177
+ unsubscribe3() // Unsubscribes ID 5
178
+ vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
179
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toBeUndefined()
180
+ expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toBeUndefined()
181
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash2]).toEqual({testSubId_3: true})
182
+ expect(state.get().documentProjections[docHandle.documentId]?.[hash2]).toEqual(projection2)
183
+
184
+ unsubscribe2() // Unsubscribes ID 4
185
+ vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
186
+ expect(state.get().subscriptions[docHandle.documentId]).toBeUndefined()
187
+ expect(state.get().documentProjections[docHandle.documentId]).toBeUndefined()
188
+ })
102
189
 
103
- const projectionState = getProjectionState({state, instance}, {document, projection})
190
+ it('resets isPending to false for the specific projection on final unsubscribe for that projection', () => {
191
+ const projectionState = getProjectionState(instance, {projection: projection1, ...docHandle})
192
+ const initialData = {name: 'Initial Name'}
193
+ const hash = hashString(projection1)
104
194
 
105
- const unsubscribe1 = projectionState.subscribe(vi.fn())
106
- const unsubscribe2 = projectionState.subscribe(vi.fn())
195
+ state.set('presetValueToPending', (prev: ProjectionStoreState) => ({
196
+ values: {
197
+ ...prev.values,
198
+ [docHandle.documentId]: {
199
+ ...prev.values[docHandle.documentId],
200
+ [hash]: {data: initialData, isPending: true},
201
+ },
202
+ },
203
+ }))
107
204
 
108
- expect(state.get().values[document._id]).toEqual({data: {field: 'Foo'}, isPending: true})
205
+ const unsubscribe1 = projectionState.subscribe(vi.fn()) // Should use ID 2
206
+ const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should use ID 3
109
207
 
110
- unsubscribe1()
111
- expect(state.get().values[document._id]).toEqual({data: {field: 'Foo'}, isPending: true})
208
+ expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
209
+ data: initialData,
210
+ isPending: true,
211
+ })
112
212
 
113
- unsubscribe2()
114
- expect(state.get().subscriptions).toEqual({})
115
- expect(state.get().values[document._id]).toEqual({data: {field: 'Foo'}, isPending: false})
116
- })
213
+ unsubscribe1() // Unsubscribes ID 2
214
+ vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
215
+ expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
216
+ data: initialData,
217
+ isPending: true,
218
+ })
219
+ expect(Object.keys(state.get().subscriptions[docHandle.documentId]?.[hash] ?? {}).length).toBe(
220
+ 1,
221
+ )
222
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash]).toEqual({testSubId_3: true})
223
+
224
+ unsubscribe2() // Unsubscribes ID 3
225
+ expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
226
+ data: initialData,
227
+ isPending: true,
228
+ })
229
+ vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
117
230
 
118
- it('calls getOrCreateResource if no state is provided', () => {
119
- ;(getOrCreateResource as Mock).mockReturnValue({state})
120
- getProjectionState(instance, {document, projection})
121
- expect(getOrCreateResource).toHaveBeenCalledWith(instance, projectionStore)
231
+ // NOW the pending state should be reset for this specific projection
232
+ expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
233
+ data: initialData,
234
+ isPending: false,
235
+ })
236
+ expect(state.get().subscriptions[docHandle.documentId]?.[hash]).toBeUndefined()
237
+ expect(state.get().documentProjections[docHandle.documentId]?.[hash]).toBeUndefined()
122
238
  })
123
239
  })
@@ -1,41 +1,35 @@
1
1
  import {omit} from 'lodash-es'
2
2
 
3
- import {type DocumentHandle} from '../document/patchOperations'
4
- import {type SanityInstance} from '../instance/types'
5
- import {type ActionContext, createAction} from '../resources/createAction'
6
- import {createStateSourceAction, type StateSource} from '../resources/createStateSourceAction'
7
- import {getPublishedId, insecureRandomId} from '../utils/ids'
3
+ import {type DocumentHandle} from '../config/sanityConfig'
4
+ import {bindActionByDataset} from '../store/createActionBinder'
5
+ import {type SanityInstance} from '../store/createSanityInstance'
8
6
  import {
9
- projectionStore,
10
- type ProjectionStoreState,
11
- type ProjectionValuePending,
12
- type ValidProjection,
13
- } from './projectionStore'
14
- import {STABLE_EMPTY_PROJECTION, validateProjection} from './util'
7
+ createStateSourceAction,
8
+ type SelectorContext,
9
+ type StateSource,
10
+ } from '../store/createStateSourceAction'
11
+ import {hashString} from '../utils/hashString'
12
+ import {getPublishedId, insecureRandomId} from '../utils/ids'
13
+ import {projectionStore} from './projectionStore'
14
+ import {type ProjectionStoreState, type ProjectionValuePending, type ValidProjection} from './types'
15
+ import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
15
16
 
16
- interface GetProjectionStateOptions {
17
- document: DocumentHandle
17
+ interface GetProjectionStateOptions extends DocumentHandle {
18
18
  projection: ValidProjection
19
19
  }
20
20
 
21
- const getProjectStateSourceAction = createStateSourceAction(
22
- projectionStore,
23
- (state, {document}: GetProjectionStateOptions): ProjectionValuePending<object> =>
24
- state.values[document._id] ?? STABLE_EMPTY_PROJECTION,
25
- )
26
-
27
21
  /**
28
22
  * @beta
29
23
  */
30
24
  export function getProjectionState<TResult extends object>(
31
- instance: SanityInstance | ActionContext<ProjectionStoreState>,
25
+ instance: SanityInstance,
32
26
  options: GetProjectionStateOptions,
33
27
  ): StateSource<ProjectionValuePending<TResult>>
34
28
  /**
35
29
  * @beta
36
30
  */
37
31
  export function getProjectionState(
38
- instance: SanityInstance | ActionContext<ProjectionStoreState>,
32
+ instance: SanityInstance,
39
33
  options: GetProjectionStateOptions,
40
34
  ): StateSource<ProjectionValuePending<Record<string, unknown>>>
41
35
  /**
@@ -50,56 +44,92 @@ export function getProjectionState(
50
44
  /**
51
45
  * @beta
52
46
  */
53
- export const _getProjectionState = createAction(projectionStore, ({state}) => {
54
- return function ({
55
- document,
56
- projection,
57
- }: GetProjectionStateOptions): StateSource<ProjectionValuePending<object>> {
58
- const {_id} = document
59
- const documentId = getPublishedId(_id)
60
- const projectionState = getProjectStateSourceAction(this, {document, projection})
61
-
62
- return {
63
- ...projectionState,
64
- subscribe: (subscriber) => {
65
- const subscriptionId = insecureRandomId()
47
+ export const _getProjectionState = bindActionByDataset(
48
+ projectionStore,
49
+ createStateSourceAction({
50
+ selector: (
51
+ {state}: SelectorContext<ProjectionStoreState>,
52
+ options: GetProjectionStateOptions,
53
+ ): ProjectionValuePending<object> => {
54
+ const documentId = getPublishedId(options.documentId)
55
+ const projectionHash = hashString(options.projection)
56
+ return state.values[documentId]?.[projectionHash] ?? STABLE_EMPTY_PROJECTION
57
+ },
58
+ onSubscribe: ({state}, {projection, ...docHandle}: GetProjectionStateOptions) => {
59
+ const subscriptionId = insecureRandomId()
60
+ const documentId = getPublishedId(docHandle.documentId)
61
+ const validProjection = validateProjection(projection)
62
+ const projectionHash = hashString(validProjection)
66
63
 
67
- state.set('addSubscription', (prev) => ({
68
- documentProjections: {
69
- ...prev.documentProjections,
70
- [documentId]: validateProjection(projection),
64
+ state.set('addSubscription', (prev) => ({
65
+ documentProjections: {
66
+ ...prev.documentProjections,
67
+ [documentId]: {
68
+ ...prev.documentProjections[documentId],
69
+ [projectionHash]: validProjection,
71
70
  },
72
- subscriptions: {
73
- ...prev.subscriptions,
74
- [documentId]: {
75
- ...prev.subscriptions[documentId],
71
+ },
72
+ subscriptions: {
73
+ ...prev.subscriptions,
74
+ [documentId]: {
75
+ ...prev.subscriptions[documentId],
76
+ [projectionHash]: {
77
+ ...prev.subscriptions[documentId]?.[projectionHash],
76
78
  [subscriptionId]: true,
77
79
  },
78
80
  },
79
- }))
81
+ },
82
+ }))
80
83
 
81
- const unsubscribe = projectionState.subscribe(subscriber)
84
+ return () => {
85
+ setTimeout(() => {
86
+ state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
87
+ const documentSubscriptionsForHash = omit(
88
+ prev.subscriptions[documentId]?.[projectionHash],
89
+ subscriptionId,
90
+ )
91
+ const hasSubscribersForProjection = !!Object.keys(documentSubscriptionsForHash).length
82
92
 
83
- return () => {
84
- unsubscribe()
93
+ const nextSubscriptions = {...prev.subscriptions}
94
+ const nextDocumentProjections = {...prev.documentProjections}
95
+ const nextValues = {...prev.values}
85
96
 
86
- state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
87
- const documentSubscriptions = omit(prev.subscriptions[documentId], subscriptionId)
88
- const hasSubscribers = !!Object.keys(documentSubscriptions).length
89
- const prevValue = prev.values[documentId]
90
- const projectionValue = prevValue?.data ? prevValue.data : null
97
+ // clean up the subscription and documentProjection if there are no subscribers
98
+ if (!hasSubscribersForProjection) {
99
+ delete nextSubscriptions[documentId]![projectionHash]
100
+ delete nextDocumentProjections[documentId]![projectionHash]
101
+
102
+ const currentProjectionValue = prev.values[documentId]?.[projectionHash]
103
+ if (currentProjectionValue && nextValues[documentId]) {
104
+ nextValues[documentId]![projectionHash] = {
105
+ data: currentProjectionValue.data,
106
+ isPending: false,
107
+ }
108
+ }
109
+ } else {
110
+ if (nextSubscriptions[documentId]) {
111
+ nextSubscriptions[documentId]![projectionHash] = documentSubscriptionsForHash
112
+ }
113
+ }
114
+
115
+ const hasAnySubscribersForDocument = Object.values(
116
+ nextSubscriptions[documentId] ?? {},
117
+ ).some((subs) => Object.keys(subs).length > 0)
118
+
119
+ if (!hasAnySubscribersForDocument) {
120
+ delete nextSubscriptions[documentId]
121
+ delete nextDocumentProjections[documentId]
122
+ // Keep nextValues[documentId] as cache
123
+ }
91
124
 
92
125
  return {
93
- subscriptions: hasSubscribers
94
- ? {...prev.subscriptions, [documentId]: documentSubscriptions}
95
- : omit(prev.subscriptions, documentId),
96
- values: hasSubscribers
97
- ? prev.values
98
- : {...prev.values, [documentId]: {data: projectionValue, isPending: false}},
126
+ subscriptions: nextSubscriptions,
127
+ documentProjections: nextDocumentProjections,
128
+ values: nextValues,
99
129
  }
100
130
  })
101
- }
102
- },
103
- }
104
- }
105
- })
131
+ }, PROJECTION_STATE_CLEAR_DELAY)
132
+ }
133
+ },
134
+ }),
135
+ )