@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1
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.
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/_internal.js +134 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +93 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_exports/_internal.d.ts +95 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +24 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +1079 -1076
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1967 -2302
- package/dist/index.js.map +1 -1
- package/package.json +48 -47
- package/src/_exports/_internal.ts +26 -0
- package/src/_exports/index.ts +120 -11
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +83 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +364 -0
- package/src/applications/userApplications.test.ts +126 -0
- package/src/applications/userApplications.ts +261 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authMode.test.ts +25 -0
- package/src/auth/authMode.ts +6 -3
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +59 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +109 -79
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +38 -12
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +673 -278
- package/src/document/documentStore.ts +162 -71
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/permissions.test.ts +105 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +639 -64
- package/src/document/reducers.ts +187 -48
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +68 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +202 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +94 -23
- package/src/presence/presenceStore.ts +85 -29
- package/src/preview/getPreviewState.test.ts +137 -0
- package/src/preview/getPreviewState.ts +69 -0
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/resolvePreview.test.ts +64 -0
- package/src/preview/resolvePreview.ts +43 -0
- package/src/preview/types.ts +10 -0
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -1
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projection/types.ts +8 -0
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +78 -63
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +87 -90
- package/src/store/createActionBinder.ts +79 -25
- package/src/store/createSanityInstance.test.ts +83 -13
- package/src/store/createSanityInstance.ts +77 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +39 -44
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +6 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
package/src/query/queryStore.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {type ResponseQueryOptions} from '@sanity/client'
|
|
2
|
+
import {type SanityQueryResult} from 'groq'
|
|
2
3
|
import {
|
|
3
4
|
catchError,
|
|
4
5
|
combineLatest,
|
|
5
|
-
defer,
|
|
6
6
|
distinctUntilChanged,
|
|
7
7
|
EMPTY,
|
|
8
8
|
filter,
|
|
@@ -23,15 +23,8 @@ import {
|
|
|
23
23
|
} from 'rxjs'
|
|
24
24
|
|
|
25
25
|
import {getClientState} from '../client/clientStore'
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
* Although this is an import dependency cycle, it is not a logical cycle:
|
|
29
|
-
* 1. queryStore uses getPerspectiveState when resolving release perspectives
|
|
30
|
-
* 2. getPerspectiveState uses releasesStore as a data source
|
|
31
|
-
* 3. releasesStore uses queryStore as a data source
|
|
32
|
-
* 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
|
|
33
|
-
*/
|
|
34
|
-
// eslint-disable-next-line import/no-cycle
|
|
26
|
+
import {observeLiveEvents} from '../client/liveEvents'
|
|
27
|
+
import {type DatasetHandle} from '../config/sanityConfig'
|
|
35
28
|
import {getPerspectiveState} from '../releases/getPerspectiveState'
|
|
36
29
|
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
37
30
|
import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
|
|
@@ -44,6 +37,7 @@ import {
|
|
|
44
37
|
import {type StoreState} from '../store/createStoreState'
|
|
45
38
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
46
39
|
import {insecureRandomId} from '../utils/ids'
|
|
40
|
+
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
47
41
|
import {
|
|
48
42
|
QUERY_STATE_CLEAR_DELAY,
|
|
49
43
|
QUERY_STORE_API_VERSION,
|
|
@@ -51,8 +45,6 @@ import {
|
|
|
51
45
|
} from './queryStoreConstants'
|
|
52
46
|
import {
|
|
53
47
|
addSubscriber,
|
|
54
|
-
cancelQuery,
|
|
55
|
-
initializeQuery,
|
|
56
48
|
type QueryStoreState,
|
|
57
49
|
removeSubscriber,
|
|
58
50
|
setLastLiveEventId,
|
|
@@ -70,7 +62,7 @@ export interface QueryOptions<
|
|
|
70
62
|
>
|
|
71
63
|
extends
|
|
72
64
|
Pick<ResponseQueryOptions, 'useCdn' | 'cache' | 'next' | 'cacheMode' | 'tag'>,
|
|
73
|
-
|
|
65
|
+
DatasetHandle<TDataset, TProjectId> {
|
|
74
66
|
query: TQuery
|
|
75
67
|
params?: Record<string, unknown>
|
|
76
68
|
}
|
|
@@ -136,11 +128,7 @@ const errorHandler = (state: StoreState<{error?: unknown}>) => {
|
|
|
136
128
|
return (error: unknown): void => state.set('setError', {error})
|
|
137
129
|
}
|
|
138
130
|
|
|
139
|
-
const listenForNewSubscribersAndFetch = ({
|
|
140
|
-
state,
|
|
141
|
-
instance,
|
|
142
|
-
key: {resource: boundResource},
|
|
143
|
-
}: StoreContext<QueryStoreState, BoundResourceKey>) => {
|
|
131
|
+
const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QueryStoreState>) => {
|
|
144
132
|
return state.observable
|
|
145
133
|
.pipe(
|
|
146
134
|
map((s) => new Set(Object.keys(s.queries))),
|
|
@@ -172,6 +160,8 @@ const listenForNewSubscribersAndFetch = ({
|
|
|
172
160
|
const {
|
|
173
161
|
query,
|
|
174
162
|
params,
|
|
163
|
+
projectId,
|
|
164
|
+
dataset,
|
|
175
165
|
tag,
|
|
176
166
|
resource,
|
|
177
167
|
perspective: perspectiveFromOptions,
|
|
@@ -183,19 +173,15 @@ const listenForNewSubscribersAndFetch = ({
|
|
|
183
173
|
const perspective$ = isReleasePerspective(perspectiveFromOptions)
|
|
184
174
|
? getPerspectiveState(instance, {
|
|
185
175
|
perspective: perspectiveFromOptions,
|
|
186
|
-
resource
|
|
176
|
+
resource,
|
|
187
177
|
}).observable.pipe(filter(Boolean))
|
|
188
178
|
: of(perspectiveFromOptions ?? QUERY_STORE_DEFAULT_PERSPECTIVE)
|
|
189
179
|
|
|
190
|
-
// Use the store's bound resource as fallback when the query key
|
|
191
|
-
// doesn't include an explicit resource. The store is scoped to a
|
|
192
|
-
// specific resource via bindActionByResource, but the captured
|
|
193
|
-
// `instance` may have a different default resource (e.g. when the
|
|
194
|
-
// store was first created by a caller that passed an explicit
|
|
195
|
-
// resource while using the root app instance).
|
|
196
180
|
const client$ = getClientState(instance, {
|
|
197
181
|
apiVersion: QUERY_STORE_API_VERSION,
|
|
198
|
-
|
|
182
|
+
projectId,
|
|
183
|
+
dataset,
|
|
184
|
+
resource,
|
|
199
185
|
}).observable
|
|
200
186
|
|
|
201
187
|
return combineLatest({
|
|
@@ -213,15 +199,19 @@ const listenForNewSubscribersAndFetch = ({
|
|
|
213
199
|
tag,
|
|
214
200
|
}),
|
|
215
201
|
),
|
|
202
|
+
tap(({result, syncTags}) => {
|
|
203
|
+
state.set('setQueryData', setQueryData(group$.key, result, syncTags))
|
|
204
|
+
}),
|
|
205
|
+
// Catch inside the per-event stream: erroring the group pipe would
|
|
206
|
+
// complete the group's subscription, and since `groupBy` above never
|
|
207
|
+
// removes its group subjects, re-adding the key would emit into a
|
|
208
|
+
// subject with no subscribers — the key could never be fetched again.
|
|
209
|
+
catchError((error) => {
|
|
210
|
+
state.set('setQueryError', setQueryError(group$.key, error))
|
|
211
|
+
return EMPTY
|
|
212
|
+
}),
|
|
216
213
|
)
|
|
217
214
|
}),
|
|
218
|
-
catchError((error) => {
|
|
219
|
-
state.set('setQueryError', setQueryError(group$.key, error))
|
|
220
|
-
return EMPTY
|
|
221
|
-
}),
|
|
222
|
-
tap(({result, syncTags}) => {
|
|
223
|
-
state.set('setQueryData', setQueryData(group$.key, result, syncTags))
|
|
224
|
-
}),
|
|
225
215
|
),
|
|
226
216
|
),
|
|
227
217
|
)
|
|
@@ -233,27 +223,14 @@ const listenToLiveClientAndSetLastLiveEventIds = ({
|
|
|
233
223
|
instance,
|
|
234
224
|
key: {resource},
|
|
235
225
|
}: StoreContext<QueryStoreState, BoundResourceKey>) => {
|
|
236
|
-
const liveMessages$ =
|
|
237
|
-
apiVersion: QUERY_STORE_API_VERSION,
|
|
226
|
+
const liveMessages$ = observeLiveEvents(instance, {
|
|
238
227
|
resource,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (error instanceof CorsOriginError) {
|
|
246
|
-
// Swallow only CORS errors in store without bubbling up so that they are handled by the Cors Error component
|
|
247
|
-
state.set('setError', {error})
|
|
248
|
-
return EMPTY
|
|
249
|
-
}
|
|
250
|
-
throw error
|
|
251
|
-
}),
|
|
252
|
-
),
|
|
253
|
-
),
|
|
254
|
-
share(),
|
|
255
|
-
filter((e) => e.type === 'message'),
|
|
256
|
-
)
|
|
228
|
+
// Surface CORS errors as store state (handled by the Cors Error
|
|
229
|
+
// component) instead of erroring the stream: a stream error here would
|
|
230
|
+
// reach the outer subscription's errorHandler, set the store-wide
|
|
231
|
+
// `state.error`, and permanently brick every query selector.
|
|
232
|
+
onCorsError: (error) => state.set('setError', {error}),
|
|
233
|
+
}).pipe(share())
|
|
257
234
|
|
|
258
235
|
return state.observable
|
|
259
236
|
.pipe(
|
|
@@ -293,6 +270,16 @@ const listenToLiveClientAndSetLastLiveEventIds = ({
|
|
|
293
270
|
*
|
|
294
271
|
* @beta
|
|
295
272
|
*/
|
|
273
|
+
export function getQueryState<
|
|
274
|
+
TQuery extends string = string,
|
|
275
|
+
TDataset extends string = string,
|
|
276
|
+
TProjectId extends string = string,
|
|
277
|
+
>(
|
|
278
|
+
instance: SanityInstance,
|
|
279
|
+
queryOptions: QueryOptions<TQuery, TDataset, TProjectId>,
|
|
280
|
+
): StateSource<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`> | undefined>
|
|
281
|
+
|
|
282
|
+
/** @beta */
|
|
296
283
|
export function getQueryState<TData>(
|
|
297
284
|
instance: SanityInstance,
|
|
298
285
|
queryOptions: QueryOptions,
|
|
@@ -328,7 +315,7 @@ const _getQueryState = bindActionByResource(
|
|
|
328
315
|
|
|
329
316
|
return () => {
|
|
330
317
|
// this runs on unsubscribe
|
|
331
|
-
|
|
318
|
+
setCleanupTimeout(
|
|
332
319
|
() => state.set('removeSubscriber', removeSubscriber(key, subscriptionId)),
|
|
333
320
|
QUERY_STATE_CLEAR_DELAY,
|
|
334
321
|
)
|
|
@@ -341,16 +328,27 @@ const _getQueryState = bindActionByResource(
|
|
|
341
328
|
* Resolves the result of a query without registering a lasting subscriber.
|
|
342
329
|
*
|
|
343
330
|
* This function fetches the result of a GROQ query and returns a promise that resolves with the query result.
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
331
|
+
* It registers a temporary subscriber for the lifetime of the resolution (released shortly after the promise
|
|
332
|
+
* settles), so the query state participates in normal cleanup even when no lasting subscriber ever attaches —
|
|
333
|
+
* e.g. when a suspended component errors before committing. This makes it ideal for use with React Suspense,
|
|
334
|
+
* where the returned promise is thrown to delay rendering until the query result becomes available.
|
|
347
335
|
* Once the promise resolves, it is expected that a real subscriber will be added via `getQueryState` to manage ongoing updates.
|
|
348
336
|
*
|
|
349
337
|
* Additionally, an optional AbortSignal can be provided to cancel the query and immediately clear the associated state
|
|
350
|
-
* if there are no active subscribers.
|
|
338
|
+
* if there are no other active subscribers.
|
|
351
339
|
*
|
|
352
340
|
* @beta
|
|
353
341
|
*/
|
|
342
|
+
export function resolveQuery<
|
|
343
|
+
TQuery extends string = string,
|
|
344
|
+
TDataset extends string = string,
|
|
345
|
+
TProjectId extends string = string,
|
|
346
|
+
>(
|
|
347
|
+
instance: SanityInstance,
|
|
348
|
+
queryOptions: ResolveQueryOptions<TQuery, TDataset, TProjectId>,
|
|
349
|
+
): Promise<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>>
|
|
350
|
+
|
|
351
|
+
/** @beta */
|
|
354
352
|
export function resolveQuery<TData>(
|
|
355
353
|
instance: SanityInstance,
|
|
356
354
|
queryOptions: ResolveQueryOptions,
|
|
@@ -366,6 +364,14 @@ const _resolveQuery = bindActionByResource(
|
|
|
366
364
|
const {getCurrent} = getQueryState(instance, normalized)
|
|
367
365
|
const key = getQueryKey(normalized)
|
|
368
366
|
|
|
367
|
+
// Hold a temporary subscription for the lifetime of this resolution so the
|
|
368
|
+
// key participates in normal cleanup. Without one, a fetch that errors
|
|
369
|
+
// while a component is suspended leaves a subscriber-less key behind — the
|
|
370
|
+
// component never commits, so no other subscriber exists — and its stored
|
|
371
|
+
// error would be rethrown on every future mount without ever refetching.
|
|
372
|
+
const subscriptionId = insecureRandomId()
|
|
373
|
+
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
374
|
+
|
|
369
375
|
const aborted$ = signal
|
|
370
376
|
? new Observable<void>((observer) => {
|
|
371
377
|
const cleanup = () => {
|
|
@@ -383,20 +389,29 @@ const _resolveQuery = bindActionByResource(
|
|
|
383
389
|
}).pipe(
|
|
384
390
|
catchError((error) => {
|
|
385
391
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
386
|
-
|
|
392
|
+
// Release immediately (not after the clear delay) so that when
|
|
393
|
+
// this was the only subscriber, the key removal tears down the
|
|
394
|
+
// in-flight request right away — e.g. when useQuery switches
|
|
395
|
+
// to a different query
|
|
396
|
+
state.set('removeSubscriber', removeSubscriber(key, subscriptionId))
|
|
387
397
|
}
|
|
388
398
|
throw error
|
|
389
399
|
}),
|
|
390
400
|
)
|
|
391
401
|
: NEVER
|
|
392
402
|
|
|
393
|
-
state.set('initializeQuery', initializeQuery(key))
|
|
394
|
-
|
|
395
403
|
const resolved$ = state.observable.pipe(
|
|
396
404
|
map(getCurrent),
|
|
397
405
|
first((i) => i !== undefined),
|
|
398
406
|
)
|
|
399
407
|
|
|
400
|
-
|
|
408
|
+
const promise = firstValueFrom(race([resolved$, aborted$]))
|
|
409
|
+
const releaseSubscriber = () => {
|
|
410
|
+
setCleanupTimeout(() => {
|
|
411
|
+
state.set('removeSubscriber', removeSubscriber(key, subscriptionId))
|
|
412
|
+
}, QUERY_STATE_CLEAR_DELAY)
|
|
413
|
+
}
|
|
414
|
+
promise.then(releaseSubscriber, releaseSubscriber)
|
|
415
|
+
return promise
|
|
401
416
|
},
|
|
402
417
|
)
|
|
@@ -2,8 +2,6 @@ import {describe, expect, it} from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
addSubscriber,
|
|
5
|
-
cancelQuery,
|
|
6
|
-
initializeQuery,
|
|
7
5
|
type QueryStoreState,
|
|
8
6
|
removeSubscriber,
|
|
9
7
|
setLastLiveEventId,
|
|
@@ -109,44 +107,4 @@ describe('Query Reducers', () => {
|
|
|
109
107
|
expect(newState.queries).not.toHaveProperty('q8')
|
|
110
108
|
})
|
|
111
109
|
})
|
|
112
|
-
|
|
113
|
-
describe('cancelQuery', () => {
|
|
114
|
-
it('should return state unchanged if key does not exist', () => {
|
|
115
|
-
const state: QueryStoreState = {queries: {}}
|
|
116
|
-
const reducer = cancelQuery('nonexistent')
|
|
117
|
-
const newState = reducer(state)
|
|
118
|
-
expect(newState).toBe(state)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('should return state unchanged if query has subscribers', () => {
|
|
122
|
-
const state: QueryStoreState = {queries: {q9: {subscribers: ['sub1']}}}
|
|
123
|
-
const reducer = cancelQuery('q9')
|
|
124
|
-
const newState = reducer(state)
|
|
125
|
-
expect(newState).toBe(state)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('should remove the query if no subscribers exist', () => {
|
|
129
|
-
const state: QueryStoreState = {queries: {q10: {subscribers: []}}}
|
|
130
|
-
const reducer = cancelQuery('q10')
|
|
131
|
-
const newState = reducer(state)
|
|
132
|
-
expect(newState.queries).not.toHaveProperty('q10')
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
describe('initializeQuery', () => {
|
|
137
|
-
it('should return state unchanged if query already exists', () => {
|
|
138
|
-
const existing = {subscribers: ['sub1']}
|
|
139
|
-
const state: QueryStoreState = {queries: {q11: existing}}
|
|
140
|
-
const reducer = initializeQuery('q11')
|
|
141
|
-
const newState = reducer(state)
|
|
142
|
-
expect(newState).toBe(state)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('should add the query with empty subscribers if it does not exist', () => {
|
|
146
|
-
const state: QueryStoreState = {queries: {}}
|
|
147
|
-
const reducer = initializeQuery('q12')
|
|
148
|
-
const newState = reducer(state)
|
|
149
|
-
expect(newState.queries['q12']).toEqual({subscribers: []})
|
|
150
|
-
})
|
|
151
|
-
})
|
|
152
110
|
})
|
package/src/query/reducers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {omitProperty} from '../utils/object'
|
|
2
2
|
|
|
3
3
|
interface QueryState {
|
|
4
4
|
syncTags?: string[]
|
|
@@ -54,22 +54,6 @@ export const removeSubscriber =
|
|
|
54
54
|
const prevQuery = prev.queries[key]
|
|
55
55
|
if (!prevQuery) return prev
|
|
56
56
|
const subscribers = prevQuery.subscribers.filter((id) => id !== subscriptionId)
|
|
57
|
-
if (!subscribers.length) return {...prev, queries:
|
|
57
|
+
if (!subscribers.length) return {...prev, queries: omitProperty(prev.queries, key)}
|
|
58
58
|
return {...prev, queries: {...prev.queries, [key]: {...prevQuery, subscribers}}}
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
export const cancelQuery =
|
|
62
|
-
(key: string) =>
|
|
63
|
-
(prev: QueryStoreState): QueryStoreState => {
|
|
64
|
-
const prevQuery = prev.queries[key]
|
|
65
|
-
if (!prevQuery) return prev
|
|
66
|
-
if (prevQuery.subscribers.length) return prev
|
|
67
|
-
return {...prev, queries: omit(prev.queries, key)}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const initializeQuery =
|
|
71
|
-
(key: string) =>
|
|
72
|
-
(prev: QueryStoreState): QueryStoreState => {
|
|
73
|
-
if (prev.queries[key]) return prev
|
|
74
|
-
return {...prev, queries: {...prev.queries, [key]: {subscribers: []}}}
|
|
75
|
-
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import {type ReleaseDocument} from '@sanity/client'
|
|
1
2
|
import {filter, firstValueFrom, of, Subject, take} from 'rxjs'
|
|
2
3
|
import {describe, expect, it, vi} from 'vitest'
|
|
3
4
|
|
|
4
|
-
import {type
|
|
5
|
-
import {getQueryState} from '../query/queryStore'
|
|
5
|
+
import {type PerspectiveHandle, type ReleasePerspective} from '../config/sanityConfig'
|
|
6
6
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
|
-
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
7
|
import {getPerspectiveState} from './getPerspectiveState'
|
|
9
|
-
import {
|
|
8
|
+
import {observeReleases} from './observeReleases'
|
|
10
9
|
|
|
11
|
-
vi.mock('
|
|
10
|
+
vi.mock('./observeReleases')
|
|
12
11
|
|
|
13
12
|
vi.mock('../client/clientStore', () => ({
|
|
14
13
|
getClientState: vi.fn(() => ({
|
|
@@ -39,17 +38,12 @@ describe('getPerspectiveState', () => {
|
|
|
39
38
|
|
|
40
39
|
// the release store is reversed in getActiveReleases to match UI elsewhere
|
|
41
40
|
const activeReleases = [release2, release1]
|
|
42
|
-
const resource: ResourceHandle['resource'] = {projectId: 'p', dataset: 'd'}
|
|
43
41
|
|
|
44
42
|
beforeEach(() => {
|
|
45
|
-
instance = createSanityInstance()
|
|
43
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
46
44
|
|
|
47
45
|
mockReleasesQuerySubject = new Subject<ReleaseDocument[] | undefined>()
|
|
48
|
-
vi.mocked(
|
|
49
|
-
subscribe: () => () => {},
|
|
50
|
-
getCurrent: () => undefined,
|
|
51
|
-
observable: mockReleasesQuerySubject.asObservable(),
|
|
52
|
-
} as StateSource<ReleaseDocument[] | undefined>)
|
|
46
|
+
vi.mocked(observeReleases).mockReturnValue(mockReleasesQuerySubject.asObservable())
|
|
53
47
|
})
|
|
54
48
|
|
|
55
49
|
afterEach(() => {
|
|
@@ -58,7 +52,7 @@ describe('getPerspectiveState', () => {
|
|
|
58
52
|
})
|
|
59
53
|
|
|
60
54
|
it('should return default perspective if no options or instance perspective is provided', async () => {
|
|
61
|
-
const state = getPerspectiveState(instance
|
|
55
|
+
const state = getPerspectiveState(instance)
|
|
62
56
|
mockReleasesQuerySubject.next([])
|
|
63
57
|
const perspective = await firstValueFrom(state.observable)
|
|
64
58
|
expect(perspective).toBe('drafts')
|
|
@@ -66,14 +60,14 @@ describe('getPerspectiveState', () => {
|
|
|
66
60
|
|
|
67
61
|
it('should return instance perspective if provided and no options perspective', async () => {
|
|
68
62
|
instance.config.perspective = 'published'
|
|
69
|
-
const state = getPerspectiveState(instance
|
|
63
|
+
const state = getPerspectiveState(instance)
|
|
70
64
|
mockReleasesQuerySubject.next([])
|
|
71
65
|
const perspective = await firstValueFrom(state.observable)
|
|
72
66
|
expect(perspective).toBe('published')
|
|
73
67
|
})
|
|
74
68
|
|
|
75
69
|
it('should return options perspective if provided', async () => {
|
|
76
|
-
const options = {perspective: 'raw'
|
|
70
|
+
const options: PerspectiveHandle = {perspective: 'raw'}
|
|
77
71
|
const state = getPerspectiveState(instance, options)
|
|
78
72
|
mockReleasesQuerySubject.next([])
|
|
79
73
|
const perspective = await firstValueFrom(state.observable)
|
|
@@ -81,7 +75,7 @@ describe('getPerspectiveState', () => {
|
|
|
81
75
|
})
|
|
82
76
|
|
|
83
77
|
it('should return undefined if release perspective is requested but no active releases', async () => {
|
|
84
|
-
const options = {perspective: {releaseName: 'release1'}
|
|
78
|
+
const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
|
|
85
79
|
const state = getPerspectiveState(instance, options)
|
|
86
80
|
mockReleasesQuerySubject.next([])
|
|
87
81
|
const perspective = await firstValueFrom(state.observable)
|
|
@@ -89,7 +83,7 @@ describe('getPerspectiveState', () => {
|
|
|
89
83
|
})
|
|
90
84
|
|
|
91
85
|
it('should calculate perspective based on active releases and releaseName', async () => {
|
|
92
|
-
const options = {perspective: {releaseName: 'release1'}
|
|
86
|
+
const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
|
|
93
87
|
const state = getPerspectiveState(instance, options)
|
|
94
88
|
mockReleasesQuerySubject.next(activeReleases)
|
|
95
89
|
|
|
@@ -103,7 +97,7 @@ describe('getPerspectiveState', () => {
|
|
|
103
97
|
})
|
|
104
98
|
|
|
105
99
|
it('should calculate perspective including multiple releases up to the specified releaseName', async () => {
|
|
106
|
-
const options = {perspective: {releaseName: 'release2'}
|
|
100
|
+
const options: PerspectiveHandle = {perspective: {releaseName: 'release2'}}
|
|
107
101
|
const state = getPerspectiveState(instance, options)
|
|
108
102
|
mockReleasesQuerySubject.next(activeReleases)
|
|
109
103
|
const perspective = await firstValueFrom(
|
|
@@ -120,7 +114,7 @@ describe('getPerspectiveState', () => {
|
|
|
120
114
|
releaseName: 'release2',
|
|
121
115
|
excludedPerspectives: ['release1', 'drafts'],
|
|
122
116
|
}
|
|
123
|
-
const options = {perspective: perspectiveConfig
|
|
117
|
+
const options: PerspectiveHandle = {perspective: perspectiveConfig}
|
|
124
118
|
const state = getPerspectiveState(instance, options)
|
|
125
119
|
mockReleasesQuerySubject.next(activeReleases)
|
|
126
120
|
const perspective = await firstValueFrom(
|
|
@@ -133,7 +127,7 @@ describe('getPerspectiveState', () => {
|
|
|
133
127
|
})
|
|
134
128
|
|
|
135
129
|
it('should throw if the specified releaseName is not found in active releases', async () => {
|
|
136
|
-
const options = {perspective: {releaseName: 'nonexistent'}
|
|
130
|
+
const options: PerspectiveHandle = {perspective: {releaseName: 'nonexistent'}}
|
|
137
131
|
const state = getPerspectiveState(instance, options)
|
|
138
132
|
mockReleasesQuerySubject.next(activeReleases)
|
|
139
133
|
|
|
@@ -148,8 +142,8 @@ describe('getPerspectiveState', () => {
|
|
|
148
142
|
})
|
|
149
143
|
|
|
150
144
|
it('should reuse the same options object for identical inputs (cache test)', async () => {
|
|
151
|
-
const options1 = {perspective: {releaseName: 'release1'}
|
|
152
|
-
const options2 = {perspective: {releaseName: 'release1'}
|
|
145
|
+
const options1: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
|
|
146
|
+
const options2: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
|
|
153
147
|
|
|
154
148
|
const state1 = getPerspectiveState(instance, options1)
|
|
155
149
|
mockReleasesQuerySubject.next(activeReleases)
|
|
@@ -167,7 +161,7 @@ describe('getPerspectiveState', () => {
|
|
|
167
161
|
})
|
|
168
162
|
|
|
169
163
|
it('should handle changes in activeReleases (cache test)', async () => {
|
|
170
|
-
const options = {perspective: {releaseName: 'release1'}
|
|
164
|
+
const options: PerspectiveHandle = {perspective: {releaseName: 'release1'}}
|
|
171
165
|
|
|
172
166
|
const state1 = getPerspectiveState(instance, options)
|
|
173
167
|
mockReleasesQuerySubject.next(activeReleases)
|
|
@@ -3,14 +3,6 @@ import {createSelector} from 'reselect'
|
|
|
3
3
|
import {type DocumentResource, type PerspectiveHandle} from '../config/sanityConfig'
|
|
4
4
|
import {bindActionByResource, type BoundStoreAction} from '../store/createActionBinder'
|
|
5
5
|
import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
|
|
6
|
-
/*
|
|
7
|
-
* Although this is an import dependency cycle, it is not a logical cycle:
|
|
8
|
-
* 1. getPerspectiveState uses releasesStore as a data source
|
|
9
|
-
* 2. releasesStore uses queryStore as a data source
|
|
10
|
-
* 3. queryStore calls getPerspectiveState for computing release perspectives
|
|
11
|
-
* 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
|
|
12
|
-
*/
|
|
13
|
-
// eslint-disable-next-line import/no-cycle
|
|
14
6
|
import {releasesStore, type ReleasesStoreState} from './releasesStore'
|
|
15
7
|
import {isReleasePerspective} from './utils/isReleasePerspective'
|
|
16
8
|
import {sortReleases} from './utils/sortReleases'
|
|
@@ -26,7 +18,7 @@ const selectActiveReleases = (context: SelectorContext<ReleasesStoreState>) =>
|
|
|
26
18
|
context.state.activeReleases
|
|
27
19
|
const selectOptions = (
|
|
28
20
|
_context: SelectorContext<ReleasesStoreState>,
|
|
29
|
-
options: PerspectiveHandle & {resource?: DocumentResource},
|
|
21
|
+
options: PerspectiveHandle & {projectId?: string; dataset?: string; resource?: DocumentResource},
|
|
30
22
|
) => options
|
|
31
23
|
|
|
32
24
|
const memoizedOptionsSelector = createSelector(
|
|
@@ -53,7 +45,6 @@ const memoizedOptionsSelector = createSelector(
|
|
|
53
45
|
},
|
|
54
46
|
)
|
|
55
47
|
|
|
56
|
-
// Lazily bind the action itself to avoid circular import initialization issues with `releasesStore`
|
|
57
48
|
const _getPerspectiveStateSelector = createStateSourceAction({
|
|
58
49
|
selector: createSelector(
|
|
59
50
|
[selectInstancePerspective, selectActiveReleases, memoizedOptionsSelector],
|
|
@@ -89,7 +80,10 @@ type BoundGetPerspectiveState = BoundStoreAction<
|
|
|
89
80
|
ReturnType<typeof _getPerspectiveStateSelector>
|
|
90
81
|
>
|
|
91
82
|
|
|
92
|
-
|
|
83
|
+
const _boundGetPerspectiveState = bindActionByResource(
|
|
84
|
+
releasesStore,
|
|
85
|
+
_getPerspectiveStateSelector,
|
|
86
|
+
) as BoundGetPerspectiveState
|
|
93
87
|
|
|
94
88
|
/**
|
|
95
89
|
* Provides a subscribable state source for a "perspective" for the Sanity client,
|
|
@@ -103,12 +97,6 @@ let _boundGetPerspectiveState: BoundGetPerspectiveState | undefined
|
|
|
103
97
|
*
|
|
104
98
|
* @public
|
|
105
99
|
*/
|
|
106
|
-
export const getPerspectiveState: BoundGetPerspectiveState = (instance, ...rest) =>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
releasesStore,
|
|
110
|
-
_getPerspectiveStateSelector,
|
|
111
|
-
) as BoundGetPerspectiveState
|
|
112
|
-
}
|
|
113
|
-
return _boundGetPerspectiveState(instance, ...rest)
|
|
114
|
-
}
|
|
100
|
+
export const getPerspectiveState: BoundGetPerspectiveState = (instance, ...rest) =>
|
|
101
|
+
// bindActionByResource keyFn destructures { resource } from the first param, so pass {} when no options
|
|
102
|
+
_boundGetPerspectiveState(instance, ...(rest.length ? rest : [{}]))
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {type LiveEventMessage, type ReleaseDocument, type SanityClient} from '@sanity/client'
|
|
2
|
+
import {of, Subject} from 'rxjs'
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {getClientState} from '../client/clientStore'
|
|
6
|
+
import {observeLiveEvents} from '../client/liveEvents'
|
|
7
|
+
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
8
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
9
|
+
import {observeReleases} from './observeReleases'
|
|
10
|
+
|
|
11
|
+
vi.mock('../client/clientStore', () => ({getClientState: vi.fn()}))
|
|
12
|
+
vi.mock('../client/liveEvents', () => ({observeLiveEvents: vi.fn()}))
|
|
13
|
+
|
|
14
|
+
describe('observeReleases', () => {
|
|
15
|
+
let instance: SanityInstance
|
|
16
|
+
let liveMessages: Subject<LiveEventMessage>
|
|
17
|
+
let fetch: ReturnType<typeof vi.fn>
|
|
18
|
+
|
|
19
|
+
const release = {
|
|
20
|
+
_id: 'r1',
|
|
21
|
+
_type: 'system.release',
|
|
22
|
+
name: 'r1',
|
|
23
|
+
metadata: {title: 'R1', releaseType: 'asap'},
|
|
24
|
+
} as ReleaseDocument
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks()
|
|
28
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
29
|
+
liveMessages = new Subject<LiveEventMessage>()
|
|
30
|
+
|
|
31
|
+
fetch = vi.fn().mockReturnValue(of({result: [release], syncTags: ['s1:tag']}))
|
|
32
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
33
|
+
observable: of({observable: {fetch}} as unknown as SanityClient),
|
|
34
|
+
} as StateSource<SanityClient>)
|
|
35
|
+
vi.mocked(observeLiveEvents).mockReturnValue(liveMessages.asObservable())
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
instance.dispose()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('emits undefined synchronously, then fetches releases through the raw perspective', () => {
|
|
43
|
+
const releases$ = observeReleases(instance, {onCorsError: vi.fn()})
|
|
44
|
+
const emissions: (ReleaseDocument[] | undefined)[] = []
|
|
45
|
+
const subscription = releases$.subscribe((releases) => emissions.push(releases))
|
|
46
|
+
|
|
47
|
+
// the synchronous undefined lets the releases store immediately record an
|
|
48
|
+
// empty list, so consumers (e.g. suspending React hooks) never hang on a
|
|
49
|
+
// fetch that has not resolved yet
|
|
50
|
+
expect(emissions).toEqual([undefined, [release]])
|
|
51
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
52
|
+
'releases::all()',
|
|
53
|
+
{},
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
perspective: 'raw',
|
|
56
|
+
tag: 'releases',
|
|
57
|
+
lastLiveEventId: undefined,
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
subscription.unsubscribe()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('refetches with the live event id when a message matches the sync tags', () => {
|
|
64
|
+
const releases$ = observeReleases(instance, {onCorsError: vi.fn()})
|
|
65
|
+
const emissions: (ReleaseDocument[] | undefined)[] = []
|
|
66
|
+
const subscription = releases$.subscribe((releases) => emissions.push(releases))
|
|
67
|
+
|
|
68
|
+
liveMessages.next({type: 'message', id: 'event-1', tags: ['s1:tag']} as LiveEventMessage)
|
|
69
|
+
|
|
70
|
+
expect(fetch).toHaveBeenCalledTimes(2)
|
|
71
|
+
expect(fetch).toHaveBeenLastCalledWith(
|
|
72
|
+
'releases::all()',
|
|
73
|
+
{},
|
|
74
|
+
expect.objectContaining({lastLiveEventId: 'event-1'}),
|
|
75
|
+
)
|
|
76
|
+
expect(emissions).toHaveLength(3) // initial undefined + two fetch results
|
|
77
|
+
subscription.unsubscribe()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('ignores live events that do not match the sync tags', () => {
|
|
81
|
+
const releases$ = observeReleases(instance, {onCorsError: vi.fn()})
|
|
82
|
+
const subscription = releases$.subscribe()
|
|
83
|
+
|
|
84
|
+
liveMessages.next({type: 'message', id: 'event-1', tags: ['s1:other']} as LiveEventMessage)
|
|
85
|
+
|
|
86
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
87
|
+
subscription.unsubscribe()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('refetches for a matching message that arrived while a fetch was still in flight', () => {
|
|
91
|
+
const fetchResponses: Subject<{result: ReleaseDocument[]; syncTags: string[]}>[] = []
|
|
92
|
+
fetch.mockImplementation(() => {
|
|
93
|
+
const response$ = new Subject<{result: ReleaseDocument[]; syncTags: string[]}>()
|
|
94
|
+
fetchResponses.push(response$)
|
|
95
|
+
return response$
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const releases$ = observeReleases(instance, {onCorsError: vi.fn()})
|
|
99
|
+
const emissions: (ReleaseDocument[] | undefined)[] = []
|
|
100
|
+
const subscription = releases$.subscribe((releases) => emissions.push(releases))
|
|
101
|
+
|
|
102
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
103
|
+
|
|
104
|
+
// A message lands while the initial fetch is in flight — its sync tags are
|
|
105
|
+
// not known yet, so it can't be evaluated right away…
|
|
106
|
+
liveMessages.next({type: 'message', id: 'event-1', tags: ['s1:tag']} as LiveEventMessage)
|
|
107
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
108
|
+
|
|
109
|
+
// …but once the fetch completes with matching tags, it must trigger a
|
|
110
|
+
// refetch instead of being dropped (which would leave the store stale)
|
|
111
|
+
fetchResponses[0].next({result: [release], syncTags: ['s1:tag']})
|
|
112
|
+
fetchResponses[0].complete()
|
|
113
|
+
|
|
114
|
+
expect(fetch).toHaveBeenCalledTimes(2)
|
|
115
|
+
expect(fetch).toHaveBeenLastCalledWith(
|
|
116
|
+
'releases::all()',
|
|
117
|
+
{},
|
|
118
|
+
expect.objectContaining({lastLiveEventId: 'event-1'}),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
fetchResponses[1].next({result: [release], syncTags: ['s1:tag']})
|
|
122
|
+
fetchResponses[1].complete()
|
|
123
|
+
expect(emissions).toHaveLength(3) // initial undefined + two fetch results
|
|
124
|
+
|
|
125
|
+
subscription.unsubscribe()
|
|
126
|
+
})
|
|
127
|
+
})
|