@sanity/sdk 2.18.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 +233 -12
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
- package/dist/_chunks-es/_internal.js +1 -0
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +284 -6
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/telemetryManager.js +2 -2
- package/dist/_exports/_internal.d.ts +2 -2
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +3 -1
- package/dist/index.d.ts +599 -185
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +492 -337
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/_exports/_internal.ts +11 -0
- package/src/_exports/index.ts +57 -12
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- 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/getOrganizationVerificationState.test.ts +7 -7
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/document/documentStore.concurrency.test.ts +313 -22
- 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 +18 -84
- package/src/organization/organization.test.ts +11 -11
- package/src/organization/organization.ts +26 -54
- package/src/organizations/organizations.test-d.ts +11 -32
- package/src/organizations/organizations.test.ts +8 -8
- package/src/organizations/organizations.ts +29 -54
- package/src/project/project.test-d.ts +19 -75
- package/src/project/project.test.ts +57 -10
- package/src/project/project.ts +33 -52
- package/src/projects/projects.test-d.ts +15 -21
- package/src/projects/projects.test.ts +8 -8
- package/src/projects/projects.ts +25 -49
- 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/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/setCleanupTimeout.ts +21 -8
- package/dist/_chunks-es/version.js +0 -7
- package/dist/_chunks-es/version.js.map +0 -1
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -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
|
+
})
|