@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,36 +1,23 @@
1
- import {type DocumentHandle} from '../document/patchOperations'
2
- import {type ActionContext, createAction} from '../resources/createAction'
1
+ import {filter, firstValueFrom} from 'rxjs'
2
+
3
+ import {type DocumentHandle} from '../config/sanityConfig'
4
+ import {bindActionByDataset} from '../store/createActionBinder'
3
5
  import {getProjectionState} from './getProjectionState'
4
- import {
5
- projectionStore,
6
- type ProjectionStoreState,
7
- type ProjectionValuePending,
8
- type ValidProjection,
9
- } from './projectionStore'
6
+ import {projectionStore, type ValidProjection} from './projectionStore'
10
7
 
11
- interface ResolveProjectionOptions {
12
- document: DocumentHandle
8
+ interface ResolveProjectionOptions extends DocumentHandle {
13
9
  projection: ValidProjection
14
10
  }
15
11
 
16
12
  /**
17
13
  * @beta
18
14
  */
19
- export const resolveProjection = createAction(projectionStore, () => {
20
- return function <TResult extends Record<string, unknown> = Record<string, unknown>>(
21
- this: ActionContext<ProjectionStoreState>,
22
- {document, projection}: ResolveProjectionOptions,
23
- ): Promise<ProjectionValuePending<TResult>> {
24
- const {getCurrent, subscribe} = getProjectionState<TResult>(this, {document, projection})
25
-
26
- return new Promise<ProjectionValuePending<TResult>>((resolve) => {
27
- const unsubscribe = subscribe(() => {
28
- const current = getCurrent()
29
- if (current?.data) {
30
- resolve(current)
31
- unsubscribe()
32
- }
33
- })
34
- })
35
- }
36
- })
15
+ export const resolveProjection = bindActionByDataset(
16
+ projectionStore,
17
+ ({instance}, {projection, ...docHandle}: ResolveProjectionOptions) =>
18
+ firstValueFrom(
19
+ getProjectionState(instance, {...docHandle, projection}).observable.pipe(
20
+ filter((i) => !!i.data),
21
+ ),
22
+ ),
23
+ )
@@ -1,51 +1,39 @@
1
- import {SanityClient, type SyncTag} from '@sanity/client'
2
- import {Observable, of, Subject} from 'rxjs'
3
- import {describe, expect, it, type Mock, vi} from 'vitest'
4
-
5
- import {getClientState} from '../client/clientStore'
6
- import {hashString} from '../common/util'
7
- import {createSanityInstance} from '../instance/sanityInstance'
8
- import {createResourceState, type ResourceState} from '../resources/createResource'
9
- import {type StateSource} from '../resources/createStateSourceAction'
1
+ import {NEVER, Observable, type Observer} from 'rxjs'
2
+ import {describe, expect, it, vi} from 'vitest'
3
+
4
+ import {getQueryState, resolveQuery} from '../query/queryStore'
5
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
+ import {type StateSource} from '../store/createStateSourceAction'
7
+ import {createStoreState, type StoreState} from '../store/createStoreState'
8
+ import {hashString} from '../utils/hashString'
10
9
  import {type ProjectionQueryResult, type ProjectionStoreState} from './projectionStore'
11
10
  import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
12
11
 
13
- vi.mock('../client/clientStore.ts', () => ({getClientState: vi.fn()}))
14
- vi.mock('../resources/createResource', async (importOriginal) => {
15
- const original = await importOriginal<typeof import('../resources/createResource')>()
16
- return {...original, getOrCreateResource: vi.fn()}
17
- })
12
+ vi.mock('../query/queryStore')
18
13
 
19
14
  describe('subscribeToStateAndFetchBatches', () => {
20
- const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
21
-
22
- let state: ResourceState<ProjectionStoreState>
23
- let fetchResults: Subject<{result: ProjectionQueryResult[]; syncTags: SyncTag[]}>
24
- let mockFetch: Mock
15
+ let instance: SanityInstance
16
+ let state: StoreState<ProjectionStoreState>
25
17
 
26
18
  beforeEach(() => {
27
- state = createResourceState<ProjectionStoreState>({
19
+ vi.clearAllMocks()
20
+ instance = createSanityInstance({projectId: 'test', dataset: 'test'})
21
+ state = createStoreState<ProjectionStoreState>({
28
22
  documentProjections: {},
29
- lastLiveEventId: null,
30
23
  subscriptions: {},
31
- syncTags: {},
32
24
  values: {},
33
25
  })
34
- fetchResults = new Subject()
35
-
36
- mockFetch = vi.fn().mockImplementation(
37
- () =>
38
- new Observable((subscriber) => {
39
- const subscription = fetchResults.subscribe({
40
- next: (val) => subscriber.next(val),
41
- complete: () => subscriber.complete(),
42
- })
43
- return () => subscription.unsubscribe()
44
- }),
45
- )
46
- vi.mocked(getClientState).mockReturnValue({
47
- observable: of({observable: {fetch: mockFetch}} as unknown as SanityClient),
48
- } as StateSource<SanityClient>)
26
+
27
+ vi.mocked(getQueryState).mockReturnValue({
28
+ getCurrent: () => undefined,
29
+ observable: NEVER as Observable<ProjectionQueryResult[] | undefined>,
30
+ } as StateSource<ProjectionQueryResult[] | undefined>)
31
+
32
+ vi.mocked(resolveQuery).mockResolvedValue(undefined)
33
+ })
34
+
35
+ afterEach(() => {
36
+ instance.dispose()
49
37
  })
50
38
 
51
39
  it('batches rapid subscription changes into single requests', async () => {
@@ -67,38 +55,82 @@ describe('subscribeToStateAndFetchBatches', () => {
67
55
  // Wait for debounce
68
56
  await new Promise((resolve) => setTimeout(resolve, 100))
69
57
 
70
- expect(mockFetch).toHaveBeenCalledTimes(1)
71
- expect(mockFetch.mock.calls[0][1]).toMatchObject({
72
- [`__ids_${projectionHash}`]: ['doc1', 'drafts.doc1', 'doc2', 'drafts.doc2'],
73
- })
58
+ expect(getQueryState).toHaveBeenCalledTimes(1)
59
+ expect(getQueryState).toHaveBeenCalledWith(
60
+ instance,
61
+ expect.any(String),
62
+ expect.objectContaining({
63
+ params: {
64
+ [`__ids_${projectionHash}`]: ['doc1', 'drafts.doc1', 'doc2', 'drafts.doc2'],
65
+ },
66
+ }),
67
+ )
74
68
 
75
69
  subscription.unsubscribe()
76
70
  })
77
71
 
78
- it('always uses latest client/eventId state when fetching', async () => {
72
+ it('processes query results and updates state with resolved values', async () => {
73
+ const teardown = vi.fn()
74
+ const subscriber = vi
75
+ .fn<(observer: Observer<ProjectionQueryResult[] | undefined>) => () => void>()
76
+ .mockReturnValue(teardown)
77
+
78
+ vi.mocked(getQueryState).mockReturnValue({
79
+ getCurrent: () => undefined,
80
+ observable: new Observable(subscriber),
81
+ } as StateSource<ProjectionQueryResult[] | undefined>)
82
+
79
83
  const subscription = subscribeToStateAndFetchBatches({instance, state})
80
84
 
85
+ expect(subscriber).not.toHaveBeenCalled()
86
+
81
87
  // Add a subscription
82
88
  state.set('addSubscription', {
83
- documentProjections: {doc1: '{title, description}'},
89
+ documentProjections: {doc1: '{title}'},
84
90
  subscriptions: {doc1: {sub1: true}},
85
91
  })
86
92
 
87
- // Update lastLiveEventId
88
- state.set('updateEventId', {lastLiveEventId: 'event1'})
93
+ expect(subscriber).not.toHaveBeenCalled()
89
94
 
90
95
  // Wait for debounce
91
96
  await new Promise((resolve) => setTimeout(resolve, 100))
92
97
 
93
- expect(mockFetch).toHaveBeenCalledWith(
94
- expect.any(String),
95
- expect.any(Object),
96
- expect.objectContaining({
97
- lastLiveEventId: 'event1',
98
- }),
99
- )
98
+ expect(subscriber).toHaveBeenCalled()
99
+ expect(teardown).not.toHaveBeenCalled()
100
+
101
+ const [observer] = subscriber.mock.lastCall!
102
+
103
+ const timestamp = new Date().toISOString()
104
+
105
+ observer.next([
106
+ {
107
+ _id: 'doc1',
108
+ _type: 'doc',
109
+ _updatedAt: timestamp,
110
+ result: {title: 'resolved'},
111
+ },
112
+ {
113
+ _id: 'drafts.doc1',
114
+ _type: 'doc',
115
+ _updatedAt: timestamp,
116
+ result: {title: 'resolved'},
117
+ },
118
+ ])
119
+
120
+ const {values} = state.get()
121
+ expect(values['doc1']).toEqual({
122
+ isPending: false,
123
+ data: {
124
+ title: 'resolved',
125
+ status: {
126
+ lastEditedDraftAt: timestamp,
127
+ lastEditedPublishedAt: timestamp,
128
+ },
129
+ },
130
+ })
100
131
 
101
132
  subscription.unsubscribe()
133
+ expect(teardown).toHaveBeenCalled()
102
134
  })
103
135
 
104
136
  it('handles new subscriptions optimistically with pending states', async () => {
@@ -133,6 +165,7 @@ describe('subscribeToStateAndFetchBatches', () => {
133
165
  })
134
166
 
135
167
  it('cancels and restarts fetches when subscription set changes', async () => {
168
+ const abortSpy = vi.spyOn(AbortController.prototype, 'abort')
136
169
  const subscription = subscribeToStateAndFetchBatches({instance, state})
137
170
 
138
171
  // Add initial subscription
@@ -151,35 +184,21 @@ describe('subscribeToStateAndFetchBatches', () => {
151
184
 
152
185
  await new Promise((resolve) => setTimeout(resolve, 100))
153
186
 
154
- expect(mockFetch).toHaveBeenCalledTimes(2)
187
+ expect(getQueryState).toHaveBeenCalledTimes(3)
188
+ expect(abortSpy).toHaveBeenCalled()
155
189
 
156
190
  subscription.unsubscribe()
157
191
  })
158
192
 
159
- it('only refetches when actually needed due to distinctUntilChanged() usage', async () => {
160
- const subscription = subscribeToStateAndFetchBatches({instance, state})
161
-
162
- // Add a subscription
163
- state.set('addSubscription', {
164
- documentProjections: {doc1: '{title, description}'},
165
- subscriptions: {doc1: {sub1: true}},
166
- })
167
-
168
- await new Promise((resolve) => setTimeout(resolve, 100))
169
-
170
- // Update state but don't change subscriptions
171
- state.set('unrelatedChange', {
172
- syncTags: {'s1:tag1': true},
173
- })
174
-
175
- await new Promise((resolve) => setTimeout(resolve, 100))
176
-
177
- expect(mockFetch).toHaveBeenCalledTimes(1)
193
+ it('processes and applies fetch results correctly', async () => {
194
+ const subscriber =
195
+ vi.fn<(observer: Observer<ProjectionQueryResult[] | undefined>) => () => void>()
178
196
 
179
- subscription.unsubscribe()
180
- })
197
+ vi.mocked(getQueryState).mockReturnValue({
198
+ getCurrent: () => undefined,
199
+ observable: new Observable(subscriber),
200
+ } as StateSource<ProjectionQueryResult[] | undefined>)
181
201
 
182
- it('processes and applies fetch results correctly', async () => {
183
202
  const subscription = subscribeToStateAndFetchBatches({instance, state})
184
203
 
185
204
  // Add a subscription
@@ -190,18 +209,18 @@ describe('subscribeToStateAndFetchBatches', () => {
190
209
 
191
210
  await new Promise((resolve) => setTimeout(resolve, 100))
192
211
 
212
+ expect(subscriber).toHaveBeenCalled()
213
+ const [observer] = subscriber.mock.lastCall!
214
+
193
215
  // Emit fetch results
194
- fetchResults.next({
195
- result: [
196
- {
197
- _id: 'doc1',
198
- _type: 'test',
199
- _updatedAt: '2024-01-01T00:00:00Z',
200
- result: {title: 'Test Document', description: 'Test Description'},
201
- },
202
- ],
203
- syncTags: ['s1:tag1', 's1:tag2'],
204
- })
216
+ observer.next([
217
+ {
218
+ _id: 'doc1',
219
+ _type: 'test',
220
+ _updatedAt: '2024-01-01T00:00:00Z',
221
+ result: {title: 'Test Document', description: 'Test Description'},
222
+ },
223
+ ])
205
224
 
206
225
  // Check that the state was updated
207
226
  expect(state.get().values['doc1']).toEqual({
@@ -214,10 +233,6 @@ describe('subscribeToStateAndFetchBatches', () => {
214
233
  }),
215
234
  isPending: false,
216
235
  })
217
- expect(state.get().syncTags).toEqual({
218
- 's1:tag1': true,
219
- 's1:tag2': true,
220
- })
221
236
 
222
237
  subscription.unsubscribe()
223
238
  })
@@ -1,104 +1,117 @@
1
- import {type SanityClient, type SyncTag} from '@sanity/client'
1
+ import {isEqual} from 'lodash-es'
2
2
  import {
3
3
  combineLatest,
4
4
  debounceTime,
5
+ defer,
5
6
  distinctUntilChanged,
6
7
  EMPTY,
8
+ filter,
9
+ from,
7
10
  map,
8
11
  Observable,
9
12
  pairwise,
10
13
  startWith,
14
+ Subscription,
11
15
  switchMap,
12
16
  tap,
13
- withLatestFrom,
14
17
  } from 'rxjs'
15
18
 
16
- import {getClientState} from '../client/clientStore'
17
- import {type ActionContext, createInternalAction} from '../resources/createAction'
19
+ import {getQueryState, resolveQuery} from '../query/queryStore'
20
+ import {type StoreContext} from '../store/defineStore'
18
21
  import {createProjectionQuery, processProjectionQuery} from './projectionQuery'
19
22
  import {type ProjectionQueryResult, type ProjectionStoreState} from './projectionStore'
20
- import {PROJECTION_TAG} from './util'
23
+ import {PROJECTION_PERSPECTIVE, PROJECTION_TAG} from './util'
21
24
 
22
25
  const BATCH_DEBOUNCE_TIME = 50
23
26
 
24
- export const subscribeToStateAndFetchBatches = createInternalAction(
25
- ({state, instance}: ActionContext<ProjectionStoreState>) => {
26
- return function () {
27
- const client$ = new Observable<SanityClient>((observer) =>
28
- getClientState(instance, {apiVersion: 'vX'}).observable.subscribe(observer),
29
- )
27
+ const isSetEqual = <T>(a: Set<T>, b: Set<T>) =>
28
+ a.size === b.size && Array.from(a).every((i) => b.has(i))
30
29
 
31
- const documentProjections$ = state.observable.pipe(
32
- map((i) => i.documentProjections),
33
- distinctUntilChanged(),
34
- )
35
- const lastLiveEventId$ = state.observable.pipe(
36
- map((i) => i.lastLiveEventId),
37
- distinctUntilChanged(),
38
- )
30
+ export const subscribeToStateAndFetchBatches = ({
31
+ state,
32
+ instance,
33
+ }: StoreContext<ProjectionStoreState>): Subscription => {
34
+ const documentProjections$ = state.observable.pipe(
35
+ map((i) => i.documentProjections),
36
+ distinctUntilChanged(),
37
+ )
39
38
 
40
- const newSubscriberIds$ = state.observable.pipe(
41
- map(({subscriptions}) => new Set(Object.keys(subscriptions))),
42
- distinctUntilChanged((a, b) =>
43
- a.size !== b.size ? false : Array.from(a).every((i) => b.has(i)),
44
- ),
45
- debounceTime(BATCH_DEBOUNCE_TIME),
46
- startWith(new Set<string>()),
47
- pairwise(),
48
- tap(([prevIds, currIds]) => {
49
- // for all new subscriptions, set their values to pending
50
- const newIds = [...currIds].filter((element) => !prevIds.has(element))
51
- state.set('updatingPending', (prev) => {
52
- const pendingValues = newIds.reduce<ProjectionStoreState['values']>((acc, id) => {
53
- const prevValue = prev.values[id]
54
- const value = prevValue?.data ? prevValue.data : null
55
- acc[id] = {data: value, isPending: true}
56
- return acc
57
- }, {})
58
- return {values: {...prev.values, ...pendingValues}}
39
+ const newSubscriberIds$ = state.observable.pipe(
40
+ map(({subscriptions}) => new Set(Object.keys(subscriptions))),
41
+ distinctUntilChanged(isSetEqual),
42
+ debounceTime(BATCH_DEBOUNCE_TIME),
43
+ startWith(new Set<string>()),
44
+ pairwise(),
45
+ tap(([prevIds, currIds]) => {
46
+ // for all new subscriptions, set their values to pending
47
+ const newIds = [...currIds].filter((element) => !prevIds.has(element))
48
+ state.set('updatingPending', (prev) => {
49
+ const pendingValues = newIds.reduce<ProjectionStoreState['values']>((acc, id) => {
50
+ const prevValue = prev.values[id]
51
+ const value = prevValue?.data ? prevValue.data : null
52
+ acc[id] = {data: value, isPending: true}
53
+ return acc
54
+ }, {})
55
+ return {values: {...prev.values, ...pendingValues}}
56
+ })
57
+ }),
58
+ map(([, ids]) => ids),
59
+ distinctUntilChanged(isSetEqual),
60
+ )
61
+
62
+ return combineLatest([newSubscriberIds$, documentProjections$])
63
+ .pipe(
64
+ distinctUntilChanged(isEqual),
65
+ switchMap(([ids, documentProjections]) => {
66
+ if (!ids.size) return EMPTY
67
+ const {query, params} = createProjectionQuery(ids, documentProjections)
68
+ const controller = new AbortController()
69
+
70
+ return new Observable<ProjectionQueryResult[]>((observer) => {
71
+ const {getCurrent, observable} = getQueryState<ProjectionQueryResult[]>(instance, query, {
72
+ params,
73
+ tag: PROJECTION_TAG,
74
+ perspective: PROJECTION_PERSPECTIVE,
59
75
  })
60
- }),
61
- withLatestFrom(documentProjections$),
62
- map(([[, ids], documentProjections]) => ({ids, documentProjections})),
63
- )
64
76
 
65
- return combineLatest([newSubscriberIds$, lastLiveEventId$, client$])
66
- .pipe(
67
- switchMap(([{ids, documentProjections}, lastLiveEventId, client]) => {
68
- if (!ids.size) return EMPTY
69
- const {query, params} = createProjectionQuery(ids, documentProjections)
77
+ const source$ = defer(() => {
78
+ if (getCurrent() === undefined) {
79
+ return from(
80
+ resolveQuery<ProjectionQueryResult[]>(instance, query, {
81
+ params,
82
+ tag: PROJECTION_TAG,
83
+ perspective: PROJECTION_PERSPECTIVE,
84
+ signal: controller.signal,
85
+ }),
86
+ ).pipe(switchMap(() => observable))
87
+ }
88
+ return observable
89
+ }).pipe(filter((result) => result !== undefined))
70
90
 
71
- return client.observable
72
- .fetch<ProjectionQueryResult[]>(query, params, {
73
- filterResponse: false,
74
- returnQuery: false,
75
- perspective: 'drafts',
76
- tag: PROJECTION_TAG,
77
- lastLiveEventId,
78
- })
79
- .pipe(map((response) => ({...response, ids})))
80
- }),
81
- map(({ids, result, syncTags}) => ({
82
- syncTags,
83
- values: processProjectionQuery({
84
- projectId: instance.identity.projectId,
85
- dataset: instance.identity.dataset,
86
- ids,
87
- results: result,
88
- }),
89
- })),
90
- )
91
- .subscribe({
92
- next: ({syncTags = [], values}) => {
93
- state.set('updateResult', (prev) => ({
94
- values: {...prev.values, ...values},
95
- syncTags: syncTags.reduce<Record<SyncTag, true>>((acc, next) => {
96
- acc[next] = true
97
- return acc
98
- }, {}),
99
- }))
100
- },
101
- })
102
- }
103
- },
104
- )
91
+ const subscription = source$.subscribe(observer)
92
+
93
+ return () => {
94
+ if (!controller.signal.aborted) {
95
+ controller.abort()
96
+ }
97
+ subscription.unsubscribe()
98
+ }
99
+ }).pipe(map((data) => ({data, ids})))
100
+ }),
101
+ map(({ids, data}) => ({
102
+ values: processProjectionQuery({
103
+ projectId: instance.config.projectId!,
104
+ dataset: instance.config.dataset!,
105
+ ids,
106
+ results: data,
107
+ }),
108
+ })),
109
+ )
110
+ .subscribe({
111
+ next: ({values}) => {
112
+ state.set('updateResult', (prev) => ({
113
+ values: {...prev.values, ...values},
114
+ }))
115
+ },
116
+ })
117
+ }
@@ -1,6 +1,8 @@
1
1
  import {type ValidProjection} from './projectionStore'
2
2
 
3
3
  export const PROJECTION_TAG = 'sdk.projection'
4
+ export const PROJECTION_PERSPECTIVE = 'drafts'
5
+ export const PROJECTION_STATE_CLEAR_DELAY = 1000
4
6
 
5
7
  export const STABLE_EMPTY_PROJECTION = {
6
8
  data: null,
@@ -1,17 +1,26 @@
1
1
  import {type SanityClient} from '@sanity/client'
2
2
  import {of} from 'rxjs'
3
- import {describe, it} from 'vitest'
3
+ import {afterEach, beforeEach, describe, it} from 'vitest'
4
4
 
5
5
  import {getClientState} from '../client/clientStore'
6
- import {createSanityInstance} from '../instance/sanityInstance'
7
- import {type StateSource} from '../resources/createStateSourceAction'
6
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
+ import {type StateSource} from '../store/createStateSourceAction'
8
8
  import {resolveProjects} from './projects'
9
9
 
10
10
  vi.mock('../client/clientStore')
11
11
 
12
12
  describe('projects', () => {
13
+ let instance: SanityInstance
14
+
15
+ beforeEach(() => {
16
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
17
+ })
18
+
19
+ afterEach(() => {
20
+ instance.dispose()
21
+ })
22
+
13
23
  it('calls the `client.observable.projects.list` method on the client and returns the result', async () => {
14
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
15
24
  const projects = [{id: 'a'}, {id: 'b'}]
16
25
  const list = vi.fn().mockReturnValue(of(projects))
17
26
 
@@ -3,11 +3,16 @@ import {switchMap} from 'rxjs'
3
3
  import {getClientState} from '../client/clientStore'
4
4
  import {createFetcherStore} from '../utils/createFetcherStore'
5
5
 
6
+ const API_VERSION = 'v2025-02-19'
7
+
6
8
  const projects = createFetcherStore({
7
9
  name: 'Projects',
8
10
  getKey: () => 'projects',
9
11
  fetcher: (instance) => () =>
10
- getClientState(instance, {apiVersion: 'vX', scope: 'global'}).observable.pipe(
12
+ getClientState(instance, {
13
+ apiVersion: API_VERSION,
14
+ scope: 'global',
15
+ }).observable.pipe(
11
16
  switchMap((client) => client.observable.projects.list({includeMembers: false})),
12
17
  ),
13
18
  })