@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2
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/README.md +12 -0
- package/dist/_exports/_internal.d.ts +163 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +332 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/_exports/agent.d.ts +65 -0
- package/dist/_exports/agent.d.ts.map +1 -0
- package/dist/_exports/agent.js +71 -0
- package/dist/_exports/agent.js.map +1 -0
- package/dist/_exports/comlink.d.ts +118 -0
- package/dist/_exports/comlink.d.ts.map +1 -0
- package/dist/_exports/comlink.js +69 -0
- package/dist/_exports/comlink.js.map +1 -0
- package/dist/authStore-CtE9bLHW.js +754 -0
- package/dist/authStore-CtE9bLHW.js.map +1 -0
- package/dist/authStore-DKFX8fn7.d.ts +3179 -0
- package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
- package/dist/clientStore-BtE2-q2i.js +90 -0
- package/dist/clientStore-BtE2-q2i.js.map +1 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
- package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
- package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
- package/dist/getEnv-D5RGkQLu.js +8 -0
- package/dist/getEnv-D5RGkQLu.js.map +1 -0
- package/dist/getNodeState-CR3__B2D.js +67 -0
- package/dist/getNodeState-CR3__B2D.js.map +1 -0
- package/dist/index.d.ts +3 -2332
- package/dist/index.js +4171 -4332
- package/dist/index.js.map +1 -1
- package/dist/logger-DAtJb5RQ.js +173 -0
- package/dist/logger-DAtJb5RQ.js.map +1 -0
- package/dist/object-CxXlSVg7.js +83 -0
- package/dist/object-CxXlSVg7.js.map +1 -0
- package/dist/queryStore-DPf9sN3V.js +1346 -0
- package/dist/queryStore-DPf9sN3V.js.map +1 -0
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
- package/dist/telemetryManager-Dq_qCkeI.js +127 -0
- package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
- package/dist/version-BChjsi4u.js +9 -0
- package/dist/version-BChjsi4u.js.map +1 -0
- package/package.json +47 -52
- package/src/_exports/_internal.ts +27 -0
- package/src/_exports/agent.ts +19 -0
- package/src/_exports/comlink.ts +16 -0
- package/src/_exports/index.ts +125 -64
- 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 +88 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +367 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +58 -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/comments/addonDatasetStore.test.ts +215 -0
- package/src/comments/addonDatasetStore.ts +249 -0
- package/src/comments/buildCommentThreads.test.ts +83 -0
- package/src/comments/buildCommentThreads.ts +54 -0
- package/src/comments/commentActions.test.ts +540 -0
- package/src/comments/commentActions.ts +555 -0
- package/src/comments/commentFieldPath.test.ts +40 -0
- package/src/comments/commentFieldPath.ts +44 -0
- package/src/comments/commentsConstants.ts +80 -0
- package/src/comments/commentsStore.test.ts +419 -0
- package/src/comments/commentsStore.ts +483 -0
- package/src/comments/normalizeComment.test.ts +116 -0
- package/src/comments/normalizeComment.ts +45 -0
- package/src/comments/observeComments.test.ts +193 -0
- package/src/comments/observeComments.ts +113 -0
- package/src/comments/reducers.test.ts +277 -0
- package/src/comments/reducers.ts +316 -0
- package/src/comments/types.ts +249 -0
- package/src/comments/weakenReferences.test.ts +47 -0
- package/src/comments/weakenReferences.ts +29 -0
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +40 -82
- 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 +35 -13
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +15 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +753 -273
- package/src/document/documentStore.ts +197 -75
- 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 +76 -2
- package/src/document/patchOperations.ts +60 -1
- package/src/document/permissions.test.ts +107 -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 +772 -64
- package/src/document/reducers.ts +205 -51
- 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.test.ts +46 -0
- package/src/document/util.ts +50 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +73 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +206 -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 +201 -9
- package/src/presence/bifurTransport.ts +98 -14
- package/src/presence/paths.test.ts +70 -0
- package/src/presence/paths.ts +41 -0
- package/src/presence/presenceStore.test.ts +915 -24
- package/src/presence/presenceStore.ts +576 -104
- package/src/presence/types.ts +175 -3
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/types.ts +0 -15
- 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 -9
- 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/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 +85 -69
- 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 +109 -90
- package/src/store/createActionBinder.ts +36 -28
- package/src/store/createSanityInstance.test.ts +15 -13
- package/src/store/createSanityInstance.ts +14 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +38 -43
- 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 +3 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {filter, firstValueFrom, timeout} from 'rxjs'
|
|
1
2
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
3
|
|
|
3
4
|
import {createSanityInstance, type SanityInstance} from './createSanityInstance'
|
|
@@ -14,7 +15,7 @@ describe('createStateSourceAction', () => {
|
|
|
14
15
|
let instance: SanityInstance
|
|
15
16
|
|
|
16
17
|
beforeEach(() => {
|
|
17
|
-
instance = createSanityInstance()
|
|
18
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
18
19
|
state = createStoreState({count: 0, items: [] as string[]}, {name: 'test-store'})
|
|
19
20
|
})
|
|
20
21
|
|
|
@@ -179,7 +180,7 @@ describe('createStateSourceAction', () => {
|
|
|
179
180
|
|
|
180
181
|
// New test: distinct contexts for same state with different instance
|
|
181
182
|
it('should create distinct contexts for same state with different instance', () => {
|
|
182
|
-
const secondInstance = createSanityInstance()
|
|
183
|
+
const secondInstance = createSanityInstance({projectId: 'test2', dataset: 'test2'})
|
|
183
184
|
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
184
185
|
|
|
185
186
|
const source1 = createStateSourceAction(selector)({state, instance, key: null})
|
|
@@ -194,4 +195,65 @@ describe('createStateSourceAction', () => {
|
|
|
194
195
|
expect(context1.instance).toBe(instance)
|
|
195
196
|
expect(context2.instance).toBe(secondInstance)
|
|
196
197
|
})
|
|
198
|
+
|
|
199
|
+
it('correctly observes values defined in the onSubscribe', async () => {
|
|
200
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
201
|
+
const source = createStateSourceAction({
|
|
202
|
+
selector: selector,
|
|
203
|
+
onSubscribe() {
|
|
204
|
+
state.set('update', {count: 1})
|
|
205
|
+
},
|
|
206
|
+
})({state, instance, key: null})
|
|
207
|
+
|
|
208
|
+
const value = await firstValueFrom(
|
|
209
|
+
source.observable.pipe(
|
|
210
|
+
filter((i) => i === 1),
|
|
211
|
+
timeout(10),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
expect(value).toBe(1)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('only invokes selector once on changes', () => {
|
|
218
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
219
|
+
const source = createStateSourceAction({selector: selector})({state, instance, key: null})
|
|
220
|
+
|
|
221
|
+
expect(selector).toBeCalledTimes(0)
|
|
222
|
+
|
|
223
|
+
// Now it should be called once:
|
|
224
|
+
const sub = source.observable.subscribe()
|
|
225
|
+
expect(selector).toBeCalledTimes(1)
|
|
226
|
+
|
|
227
|
+
// The observable should be shared so this shouldn't invoke it first.
|
|
228
|
+
const sub2 = source.observable.subscribe()
|
|
229
|
+
expect(selector).toBeCalledTimes(1)
|
|
230
|
+
|
|
231
|
+
// Updating the value should only invoke it once:
|
|
232
|
+
state.set('update', {count: 1})
|
|
233
|
+
expect(selector).toBeCalledTimes(2)
|
|
234
|
+
|
|
235
|
+
sub2.unsubscribe()
|
|
236
|
+
sub.unsubscribe()
|
|
237
|
+
|
|
238
|
+
// Once everyone has unsubscribed it should be invoked again.
|
|
239
|
+
const sub3 = source.observable.subscribe()
|
|
240
|
+
expect(selector).toBeCalledTimes(3)
|
|
241
|
+
sub3.unsubscribe()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('only subscribes once when mixing subscribe/observable', () => {
|
|
245
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
246
|
+
const source = createStateSourceAction({selector: selector})({state, instance, key: null})
|
|
247
|
+
|
|
248
|
+
expect(selector).toBeCalledTimes(0)
|
|
249
|
+
|
|
250
|
+
const sub = source.observable.subscribe()
|
|
251
|
+
expect(selector).toBeCalledTimes(1)
|
|
252
|
+
|
|
253
|
+
const sub2 = source.subscribe()
|
|
254
|
+
expect(selector).toBeCalledTimes(1)
|
|
255
|
+
|
|
256
|
+
sub.unsubscribe()
|
|
257
|
+
sub2()
|
|
258
|
+
})
|
|
197
259
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {distinctUntilChanged, map, Observable,
|
|
1
|
+
import {defer, distinctUntilChanged, finalize, map, Observable, shareReplay, skip} from 'rxjs'
|
|
2
2
|
|
|
3
3
|
import {type StoreAction} from './createActionBinder'
|
|
4
4
|
import {type SanityInstance} from './createSanityInstance'
|
|
@@ -57,9 +57,9 @@ export interface StateSource<T> {
|
|
|
57
57
|
* ```ts
|
|
58
58
|
* // Using both state and instance in a selector (psuedo example)
|
|
59
59
|
* const getUserByProjectId = createStateSourceAction(
|
|
60
|
-
* ({ state, instance }: SelectorContext<UsersState>, options?:
|
|
60
|
+
* ({ state, instance }: SelectorContext<UsersState>, options?: ProjectHandle) => {
|
|
61
61
|
* const allUsers = state.users
|
|
62
|
-
* const projectId = options?.projectId ?? instance.config.
|
|
62
|
+
* const projectId = options?.projectId ?? instance.config.projectId
|
|
63
63
|
* return allUsers.filter(user => user.projectId === projectId)
|
|
64
64
|
* }
|
|
65
65
|
* )
|
|
@@ -157,11 +157,11 @@ interface StateSourceOptions<TState, TParams extends unknown[], TReturn, TKey> {
|
|
|
157
157
|
*
|
|
158
158
|
* const getDocument = bindActionByResource(
|
|
159
159
|
* documentStore,
|
|
160
|
-
* createStateSourceAction(({state},
|
|
160
|
+
* createStateSourceAction(({state}, documentId: string) => state.documents[documentId])
|
|
161
161
|
* )
|
|
162
162
|
*
|
|
163
163
|
* // Usage
|
|
164
|
-
* const documentSource = getDocument(sanityInstance,
|
|
164
|
+
* const documentSource = getDocument(sanityInstance, 'doc123')
|
|
165
165
|
* const doc = documentSource.getCurrent()
|
|
166
166
|
* const subscription = documentSource.observable.subscribe(updatedDoc => {
|
|
167
167
|
* console.log('Document changed:', updatedDoc)
|
|
@@ -187,8 +187,7 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
|
|
|
187
187
|
function stateSourceAction(context: StoreContext<TState, TKey>, ...params: TParams) {
|
|
188
188
|
const {state, instance} = context
|
|
189
189
|
|
|
190
|
-
const getCurrent = () => {
|
|
191
|
-
const currentState = state.get()
|
|
190
|
+
const getCurrent = (currentState: TState) => {
|
|
192
191
|
if (typeof currentState !== 'object' || currentState === null) {
|
|
193
192
|
throw new Error(
|
|
194
193
|
`Expected store state to be an object but got "${typeof currentState}" instead`,
|
|
@@ -208,53 +207,49 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
|
|
|
208
207
|
return selector(selectorContext, ...params)
|
|
209
208
|
}
|
|
210
209
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const cleanup = subscribeHandler?.(context, ...params)
|
|
210
|
+
// `state.observable` will emit the current value immediately and
|
|
211
|
+
// hence we inherit the same behavior here.
|
|
212
|
+
let values = state.observable.pipe(map(getCurrent), distinctUntilChanged(isEqual))
|
|
215
213
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
if (subscribeHandler) {
|
|
215
|
+
values = withSubscribeHook(values, () => subscribeHandler(context, ...params))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Share but replay the latest value so every subscriber gets an
|
|
219
|
+
// initial synchronous emission, matching `state.observable`. That keeps
|
|
220
|
+
// `skip(1)` in `subscribe()` aligned with "skip current snapshot" rather than
|
|
221
|
+
// silently eating the first real update after multicasting.
|
|
222
|
+
const sharedValues = values.pipe(shareReplay({bufferSize: 1, refCount: true}))
|
|
223
|
+
|
|
224
|
+
const subscribe = (onStoreChanged?: () => void) => {
|
|
225
|
+
const subscription = sharedValues.pipe(skip(1)).subscribe({
|
|
226
|
+
next: () => onStoreChanged?.(),
|
|
227
|
+
// Propagate selector errors to both subscription types
|
|
228
|
+
error: () => onStoreChanged?.(),
|
|
229
|
+
})
|
|
231
230
|
|
|
232
231
|
return () => {
|
|
233
232
|
subscription.unsubscribe()
|
|
234
|
-
cleanup?.()
|
|
235
233
|
}
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
// Create shared observable that handles multiple subscribers efficiently
|
|
239
|
-
const observable = new Observable<TReturn>((observer) => {
|
|
240
|
-
const emitCurrent = () => {
|
|
241
|
-
try {
|
|
242
|
-
observer.next(getCurrent())
|
|
243
|
-
} catch (error) {
|
|
244
|
-
observer.error(error)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Emit immediately on subscription
|
|
248
|
-
emitCurrent()
|
|
249
|
-
return subscribe(emitCurrent)
|
|
250
|
-
}).pipe(share())
|
|
251
|
-
|
|
252
236
|
return {
|
|
253
|
-
getCurrent,
|
|
237
|
+
getCurrent: () => getCurrent(state.get()),
|
|
254
238
|
subscribe,
|
|
255
|
-
observable,
|
|
239
|
+
observable: sharedValues,
|
|
256
240
|
}
|
|
257
241
|
}
|
|
258
242
|
|
|
259
243
|
return stateSourceAction
|
|
260
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates a new Observable which wraps an existing Observable which will invoke
|
|
248
|
+
* the function when a new subscriber appears.
|
|
249
|
+
*/
|
|
250
|
+
function withSubscribeHook<T>(obs: Observable<T>, fn: () => void | (() => void)): Observable<T> {
|
|
251
|
+
return defer(() => {
|
|
252
|
+
const cleanup = fn()
|
|
253
|
+
return cleanup ? obs.pipe(finalize(() => cleanup())) : obs
|
|
254
|
+
})
|
|
255
|
+
}
|
|
@@ -13,12 +13,14 @@ describe('createStoreInstance', () => {
|
|
|
13
13
|
randomUUID: () => 'test-uuid-1234',
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
instance = createSanityInstance()
|
|
16
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
const storeDef: StoreDefinition<{count: number}> = {
|
|
20
20
|
name: 'TestStore',
|
|
21
|
-
getInitialState: () => ({
|
|
21
|
+
getInitialState: (inst) => ({
|
|
22
|
+
count: inst.config.projectId === 'test' ? 0 : -1,
|
|
23
|
+
}),
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
it('should create store instance with initial state', () => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {of} from 'rxjs'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {type SanityInstance} from './createSanityInstance'
|
|
5
|
+
import {
|
|
6
|
+
defineFetcher,
|
|
7
|
+
defineMutation,
|
|
8
|
+
type Fetcher,
|
|
9
|
+
type FetcherSnapshot,
|
|
10
|
+
type MutationResult,
|
|
11
|
+
} from './fetcherStore'
|
|
12
|
+
|
|
13
|
+
declare const instance: SanityInstance
|
|
14
|
+
|
|
15
|
+
interface Widget {
|
|
16
|
+
id: string
|
|
17
|
+
label: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// No explicit generics anywhere below: `TData` is inferred from `fetch`'s
|
|
21
|
+
// `Observable<T>` and `TParams` from the annotated `getKey`/`fetch` params —
|
|
22
|
+
// the whole handle is inferred from the definition, TanStack-style.
|
|
23
|
+
|
|
24
|
+
// A list fetcher: no params, resolves to Widget[].
|
|
25
|
+
const widgets = defineFetcher({
|
|
26
|
+
name: 'test-d-widgets',
|
|
27
|
+
getKey: () => 'all',
|
|
28
|
+
fetch: () => () => of<Widget[]>([]),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// A detail fetcher: one string param, resolves to Widget.
|
|
32
|
+
const widget = defineFetcher({
|
|
33
|
+
name: 'test-d-widget',
|
|
34
|
+
getKey: (_i, id) => id,
|
|
35
|
+
fetch: () => (id: string) => of<Widget>({id, label: ''}),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('the handle is inferred from the definition — no explicit generics', () => {
|
|
39
|
+
expectTypeOf(widgets).toEqualTypeOf<Fetcher<[], Widget[]>>()
|
|
40
|
+
expectTypeOf(widget).toEqualTypeOf<Fetcher<[id: string], Widget>>()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('unannotated getKey params are inferred (not any) from the fetcher', () => {
|
|
44
|
+
defineFetcher({
|
|
45
|
+
name: 'test-d-getkey-infer',
|
|
46
|
+
// `id` is not annotated here — it must be inferred as `string` from `fetch`.
|
|
47
|
+
getKey: (_i, id) => {
|
|
48
|
+
expectTypeOf(id).toEqualTypeOf<string>()
|
|
49
|
+
return id
|
|
50
|
+
},
|
|
51
|
+
fetch: () => (id: string) => of<Widget>({id, label: ''}),
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('getState carries TData through the snapshot envelope (not unknown)', () => {
|
|
56
|
+
expectTypeOf(widgets.getState(instance).getCurrent()).toEqualTypeOf<FetcherSnapshot<Widget[]>>()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('the snapshot discriminates data by status', () => {
|
|
60
|
+
const snap = widget.getState(instance, 'w1').getCurrent()
|
|
61
|
+
if (snap.status === 'success') {
|
|
62
|
+
expectTypeOf(snap.data).toEqualTypeOf<Widget>()
|
|
63
|
+
expectTypeOf(snap.dataUpdatedAt).toEqualTypeOf<number>()
|
|
64
|
+
}
|
|
65
|
+
if (snap.status === 'pending') {
|
|
66
|
+
expectTypeOf(snap.data).toEqualTypeOf<undefined>()
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('resolveState and refetch resolve to TData', () => {
|
|
71
|
+
expectTypeOf(widgets.resolveState(instance)).resolves.toEqualTypeOf<Widget[]>()
|
|
72
|
+
expectTypeOf(widget.refetch(instance, 'w1')).resolves.toEqualTypeOf<Widget>()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('the params tuple is enforced per fetcher', () => {
|
|
76
|
+
// @ts-expect-error — detail fetcher requires an id
|
|
77
|
+
void widget.getState(instance)
|
|
78
|
+
// @ts-expect-error — id must be a string
|
|
79
|
+
void widget.getState(instance, 123)
|
|
80
|
+
// @ts-expect-error — list fetcher takes no params
|
|
81
|
+
void widgets.getState(instance, 'nope')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('setData updater is typed to TData', () => {
|
|
85
|
+
widget.setData(instance, ['w1'], {id: 'w1', label: 'x'})
|
|
86
|
+
widget.setData(instance, ['w1'], (current) => {
|
|
87
|
+
expectTypeOf(current).toEqualTypeOf<Widget | undefined>()
|
|
88
|
+
return {id: 'w1', label: 'y'}
|
|
89
|
+
})
|
|
90
|
+
// @ts-expect-error — value must be a full Widget
|
|
91
|
+
widget.setData(instance, ['w1'], {id: 'w1'})
|
|
92
|
+
// @ts-expect-error — updater function must return TData
|
|
93
|
+
widget.setData(instance, ['w1'], () => 42)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('defineMutation resolves to a MutationResult of TResult with a typed input', () => {
|
|
97
|
+
const createWidget = defineMutation<{label: string}, Widget>({
|
|
98
|
+
name: 'test-d-create-widget',
|
|
99
|
+
mutationFn: () => (input) => {
|
|
100
|
+
expectTypeOf(input).toEqualTypeOf<{label: string}>()
|
|
101
|
+
return of<Widget>({id: 'x', label: input.label})
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
expectTypeOf(createWidget(instance, {label: 'a'})).resolves.toEqualTypeOf<
|
|
105
|
+
MutationResult<Widget>
|
|
106
|
+
>()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('CacheWriter enforces the target fetcher’s TData and TParams', () => {
|
|
110
|
+
defineMutation<void, void>({
|
|
111
|
+
name: 'test-d-write-through',
|
|
112
|
+
mutationFn: () => () => of(undefined),
|
|
113
|
+
onMutate: (write) => {
|
|
114
|
+
write(widget, ['w1'], {id: 'w1', label: 'ok'})
|
|
115
|
+
// @ts-expect-error — value must match the fetcher's TData (Widget)
|
|
116
|
+
write(widget, ['w1'], {nope: true})
|
|
117
|
+
// @ts-expect-error — params must match the fetcher's TParams ([string])
|
|
118
|
+
write(widget, [], {id: 'w1', label: 'ok'})
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {NEVER, of, startWith, Subject, throwError} from 'rxjs'
|
|
2
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {createSanityInstance, type SanityInstance} from './createSanityInstance'
|
|
5
|
+
import {defineFetcher, defineMutation, type Fetcher} from './fetcherStore'
|
|
6
|
+
|
|
7
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
8
|
+
|
|
9
|
+
describe('fetcherStore', () => {
|
|
10
|
+
let instance: SanityInstance
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
14
|
+
vi.useRealTimers()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
instance.dispose()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('fetches on subscribe and exposes a success snapshot', async () => {
|
|
22
|
+
const fetcher = defineFetcher<[number], string>({
|
|
23
|
+
name: 'basic',
|
|
24
|
+
getKey: (_i, id) => `${id}`,
|
|
25
|
+
fetch: () => (id) => of(`data-${id}`),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const data = await fetcher.resolveState(instance, 1)
|
|
29
|
+
expect(data).toBe('data-1')
|
|
30
|
+
expect(fetcher.getState(instance, 1).getCurrent()).toMatchObject({
|
|
31
|
+
status: 'success',
|
|
32
|
+
data: 'data-1',
|
|
33
|
+
isFetching: false,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('serves fresh data without refetching, and revalidates once stale', async () => {
|
|
38
|
+
const fetchSpy = vi.fn((id: number) => of(`v-${id}`))
|
|
39
|
+
const fetcher = defineFetcher<[number], string>({
|
|
40
|
+
name: 'swr',
|
|
41
|
+
staleTime: 10_000,
|
|
42
|
+
getKey: (_i, id) => `${id}`,
|
|
43
|
+
fetch: () => fetchSpy,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const first = fetcher.getState(instance, 1)
|
|
47
|
+
const unsub1 = first.subscribe()
|
|
48
|
+
await vi.waitFor(() => expect(first.getCurrent().status).toBe('success'))
|
|
49
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
50
|
+
unsub1()
|
|
51
|
+
|
|
52
|
+
// Re-subscribe within staleTime — fresh, so no refetch.
|
|
53
|
+
const second = fetcher.getState(instance, 1)
|
|
54
|
+
const unsub2 = second.subscribe()
|
|
55
|
+
await flush()
|
|
56
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
57
|
+
unsub2()
|
|
58
|
+
|
|
59
|
+
// Invalidation forces staleness — an active subscriber revalidates.
|
|
60
|
+
const third = fetcher.getState(instance, 1)
|
|
61
|
+
const unsub3 = third.subscribe()
|
|
62
|
+
await flush()
|
|
63
|
+
fetcher.invalidate(instance, 1)
|
|
64
|
+
await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(2))
|
|
65
|
+
unsub3()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('invalidate refetches an actively-subscribed entry', async () => {
|
|
69
|
+
let n = 0
|
|
70
|
+
const fetcher = defineFetcher<[], number>({
|
|
71
|
+
name: 'invalidate',
|
|
72
|
+
getKey: () => 'k',
|
|
73
|
+
fetch: () => () => of(++n),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const source = fetcher.getState(instance)
|
|
77
|
+
const unsub = source.subscribe()
|
|
78
|
+
await vi.waitFor(() => expect(source.getCurrent().data).toBe(1))
|
|
79
|
+
|
|
80
|
+
fetcher.invalidate(instance)
|
|
81
|
+
await vi.waitFor(() => expect(source.getCurrent().data).toBe(2))
|
|
82
|
+
unsub()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('refetch bypasses staleness and resolves with fresh data', async () => {
|
|
86
|
+
let n = 0
|
|
87
|
+
const fetcher = defineFetcher<[], number>({
|
|
88
|
+
name: 'refetch',
|
|
89
|
+
staleTime: 60_000,
|
|
90
|
+
getKey: () => 'k',
|
|
91
|
+
fetch: () => () => of(++n),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(await fetcher.resolveState(instance)).toBe(1)
|
|
95
|
+
expect(await fetcher.refetch(instance)).toBe(2)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('surfaces an initial failure as an error snapshot, background failure on the success arm', async () => {
|
|
99
|
+
let attempt = 0
|
|
100
|
+
const fetcher = defineFetcher<[], string>({
|
|
101
|
+
name: 'errors',
|
|
102
|
+
getKey: () => 'k',
|
|
103
|
+
fetch: () => () => (attempt++ === 0 ? throwError(() => new Error('boom')) : of('recovered')),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
await expect(fetcher.resolveState(instance)).rejects.toThrow('boom')
|
|
107
|
+
expect(fetcher.getState(instance).getCurrent().status).toBe('error')
|
|
108
|
+
|
|
109
|
+
// Now succeed, then fail a background revalidation: data stays, error attaches.
|
|
110
|
+
await fetcher.refetch(instance)
|
|
111
|
+
expect(fetcher.getState(instance).getCurrent()).toMatchObject({
|
|
112
|
+
status: 'success',
|
|
113
|
+
data: 'recovered',
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('pre-seeds an entry via initialData without fetching when the seed is fresh', async () => {
|
|
118
|
+
const fetchSpy = vi.fn(() => of('fetched'))
|
|
119
|
+
const fetcher = defineFetcher<[], string>({
|
|
120
|
+
name: 'seeded',
|
|
121
|
+
staleTime: 10_000,
|
|
122
|
+
getKey: () => 'k',
|
|
123
|
+
fetch: () => fetchSpy,
|
|
124
|
+
initialData: () => ({data: 'seeded', dataUpdatedAt: Date.now()}),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const source = fetcher.getState(instance)
|
|
128
|
+
const unsub = source.subscribe()
|
|
129
|
+
await flush()
|
|
130
|
+
|
|
131
|
+
expect(source.getCurrent()).toMatchObject({status: 'success', data: 'seeded'})
|
|
132
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
133
|
+
unsub()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('setData writes optimistically and can be undone', () => {
|
|
137
|
+
const fetcher = defineFetcher<[], string>({
|
|
138
|
+
name: 'setdata',
|
|
139
|
+
getKey: () => 'k',
|
|
140
|
+
fetch: () => () => NEVER,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const {undo} = fetcher.setData(instance, [], 'written')
|
|
144
|
+
expect(fetcher.getState(instance).getCurrent().data).toBe('written')
|
|
145
|
+
|
|
146
|
+
undo()
|
|
147
|
+
expect(fetcher.getState(instance).getCurrent().status).toBe('pending')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('garbage-collects an entry written without a subscriber after gcTime', async () => {
|
|
151
|
+
const fetcher = defineFetcher<[], string>({
|
|
152
|
+
name: 'gc-orphan',
|
|
153
|
+
gcTime: 5,
|
|
154
|
+
getKey: () => 'k',
|
|
155
|
+
fetch: () => () => NEVER,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// No subscriber ever holds this entry (as an optimistic mutation write would),
|
|
159
|
+
// so only setData's own GC scheduling can clean it up.
|
|
160
|
+
fetcher.setData(instance, [], 'written')
|
|
161
|
+
expect(fetcher.getState(instance).getCurrent().data).toBe('written')
|
|
162
|
+
|
|
163
|
+
await vi.waitFor(() => expect(fetcher.getState(instance).getCurrent().status).toBe('pending'))
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('rolls back optimistic writes when a mutation fails', async () => {
|
|
167
|
+
const fetcher = defineFetcher<[], string>({
|
|
168
|
+
name: 'mutate-rollback',
|
|
169
|
+
getKey: () => 'k',
|
|
170
|
+
fetch: () => () => NEVER,
|
|
171
|
+
})
|
|
172
|
+
fetcher.setData(instance, [], 'server')
|
|
173
|
+
|
|
174
|
+
const failing = defineMutation<string, never>({
|
|
175
|
+
name: 'rename-fail',
|
|
176
|
+
mutationFn: () => () => throwError(() => new Error('rejected')),
|
|
177
|
+
onMutate: (write, value) => write(fetcher, [], value),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
await expect(failing(instance, 'optimistic')).rejects.toThrow('rejected')
|
|
181
|
+
expect(fetcher.getState(instance).getCurrent().data).toBe('server')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('reconciles tagged entries after a successful mutation', async () => {
|
|
185
|
+
let n = 0
|
|
186
|
+
const list = defineFetcher<[], number>({
|
|
187
|
+
name: 'list',
|
|
188
|
+
getKey: () => 'all',
|
|
189
|
+
tags: [{type: 'thing', id: 'LIST'}],
|
|
190
|
+
fetch: () => () => of(++n),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const source = list.getState(instance)
|
|
194
|
+
const unsub = source.subscribe()
|
|
195
|
+
await vi.waitFor(() => expect(source.getCurrent().data).toBe(1))
|
|
196
|
+
|
|
197
|
+
const create = defineMutation<void, {ok: true}>({
|
|
198
|
+
name: 'create-thing',
|
|
199
|
+
mutationFn: () => () => of({ok: true}),
|
|
200
|
+
invalidates: [{type: 'thing', id: 'LIST'}],
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const {invalidated} = await create(instance, undefined)
|
|
204
|
+
await invalidated
|
|
205
|
+
expect(source.getCurrent().data).toBe(2)
|
|
206
|
+
unsub()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('discards a revalidation result that lands after an optimistic write', async () => {
|
|
210
|
+
const server = new Subject<string>()
|
|
211
|
+
const fetcher: Fetcher<[], string> = defineFetcher<[], string>({
|
|
212
|
+
name: 'version-guard',
|
|
213
|
+
staleTime: 0,
|
|
214
|
+
getKey: () => 'k',
|
|
215
|
+
fetch: () => () => server.pipe(startWith('initial')),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const source = fetcher.getState(instance)
|
|
219
|
+
const unsub = source.subscribe()
|
|
220
|
+
await vi.waitFor(() => expect(source.getCurrent().data).toBe('initial'))
|
|
221
|
+
|
|
222
|
+
// Force a refetch (in flight, no value yet), then write optimistically.
|
|
223
|
+
const refetching = fetcher.refetch(instance)
|
|
224
|
+
fetcher.setData(instance, [], 'optimistic')
|
|
225
|
+
expect(source.getCurrent().data).toBe('optimistic')
|
|
226
|
+
|
|
227
|
+
// Late server value must NOT clobber the optimistic write.
|
|
228
|
+
server.next('late-server')
|
|
229
|
+
await refetching
|
|
230
|
+
expect(source.getCurrent().data).toBe('optimistic')
|
|
231
|
+
unsub()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('settles a pre-empted in-flight fetch when a forced refetch replaces it', async () => {
|
|
235
|
+
const server = new Subject<string>()
|
|
236
|
+
const fetcher = defineFetcher<[], string>({
|
|
237
|
+
name: 'preempt',
|
|
238
|
+
getKey: () => 'k',
|
|
239
|
+
fetch: () => () => server,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// First refetch is in flight with no value yet; a second forced refetch
|
|
243
|
+
// pre-empts it. Both promises must settle rather than hang.
|
|
244
|
+
const first = fetcher.refetch(instance)
|
|
245
|
+
const second = fetcher.refetch(instance)
|
|
246
|
+
|
|
247
|
+
server.next('data')
|
|
248
|
+
server.complete()
|
|
249
|
+
|
|
250
|
+
await expect(first).resolves.toBe('data')
|
|
251
|
+
await expect(second).resolves.toBe('data')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('clears isFetching when a discarded revalidation runs on a non-completing source', async () => {
|
|
255
|
+
const server = new Subject<string>()
|
|
256
|
+
const fetcher = defineFetcher<[], string>({
|
|
257
|
+
name: 'version-guard-fetching',
|
|
258
|
+
staleTime: 0,
|
|
259
|
+
getKey: () => 'k',
|
|
260
|
+
fetch: () => () => server,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const source = fetcher.getState(instance)
|
|
264
|
+
const unsub = source.subscribe()
|
|
265
|
+
await vi.waitFor(() => expect(source.getCurrent().isFetching).toBe(true))
|
|
266
|
+
|
|
267
|
+
// Optimistic write bumps the version while the fetch is still in flight.
|
|
268
|
+
fetcher.setData(instance, [], 'optimistic')
|
|
269
|
+
|
|
270
|
+
// The late server value is discarded (version guard) — and because the source
|
|
271
|
+
// never completes, isFetching must be cleared here, not left stuck true.
|
|
272
|
+
server.next('late')
|
|
273
|
+
await flush()
|
|
274
|
+
expect(source.getCurrent()).toMatchObject({data: 'optimistic', isFetching: false})
|
|
275
|
+
unsub()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('polls on refetchInterval while an entry has subscribers', async () => {
|
|
279
|
+
let n = 0
|
|
280
|
+
const fetcher = defineFetcher<[], number>({
|
|
281
|
+
name: 'polling',
|
|
282
|
+
staleTime: 60_000,
|
|
283
|
+
refetchInterval: 10,
|
|
284
|
+
getKey: () => 'k',
|
|
285
|
+
fetch: () => () => of(++n),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const source = fetcher.getState(instance)
|
|
289
|
+
const unsub = source.subscribe()
|
|
290
|
+
await vi.waitFor(() => expect((source.getCurrent().data ?? 0) >= 1).toBe(true))
|
|
291
|
+
|
|
292
|
+
// Fresh per staleTime, so any further increase can only come from the interval.
|
|
293
|
+
const before = source.getCurrent().data ?? 0
|
|
294
|
+
await vi.waitFor(() => expect((source.getCurrent().data ?? 0) > before).toBe(true), {
|
|
295
|
+
timeout: 500,
|
|
296
|
+
})
|
|
297
|
+
unsub()
|
|
298
|
+
})
|
|
299
|
+
})
|