Skip to content

Commit 7ce07ae

Browse files
committed
feat(plugin): auto abort fetchFirst & fetchMany
1 parent b727f31 commit 7ce07ae

File tree

7 files changed

+280
-32
lines changed

7 files changed

+280
-32
lines changed

docs/guide/plugin/hooks.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ hook('fetchFirst', (payload) => {
4949
Markers are used to remember if a query has already been fetched or not where it is not based on the item key. For example, if you have a query that fetches all items with a certain filter, the marker is used to remember if the query has already been fetched or not.
5050
:::
5151

52+
::: warning Auto-abort remaining callbacks
53+
If a non-null result is set with `setResult`, the remaining callbacks for this hook will not be called by default. This is useful in case you have multiple plugins that can fetch the same collections (for example, one local and one remote). The first plugin to set a non-null result will abort the remaining callbacks.
54+
55+
You can override this behavior by passing `{ abort: false }` as the second argument to `setResult`.
56+
57+
```ts
58+
setResult(result, { abort: false })
59+
```
60+
:::
61+
5262
Example:
5363

5464
::: code-group
@@ -110,6 +120,16 @@ hook('fetchMany', (payload) => {
110120
})
111121
```
112122

123+
::: warning Auto-abort remaining callbacks
124+
If a non-null result is set with `setResult`, the remaining callbacks for this hook will not be called by default. This is useful in case you have multiple plugins that can fetch the same collections (for example, one local and one remote). The first plugin to set a non-null result will abort the remaining callbacks.
125+
126+
You can override this behavior by passing `{ abort: false }` as the second argument to `setResult`.
127+
128+
```ts
129+
setResult(result, { abort: false })
130+
```
131+
:::
132+
113133
Example:
114134

115135
::: code-group

packages/core/src/query/findFirst.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,19 @@ async function _findFirst<
9696
},
9797
})
9898

99+
const abort = store.$hooks.withAbort()
99100
await store.$hooks.callHook('fetchFirst', {
100101
store,
101102
meta,
102103
collection,
103104
key: findOptions.key,
104105
findOptions,
105106
getResult: () => result,
106-
setResult: (value) => {
107+
setResult: (value, options) => {
107108
result = value
109+
if (result && options?.abort !== false) {
110+
abort()
111+
}
108112
},
109113
setMarker: (value) => {
110114
marker = value

packages/core/src/query/findMany.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async function _findMany<
6161
findOptions = findOptions ?? {}
6262
const fetchPolicy = store.$getFetchPolicy(findOptions.fetchPolicy)
6363

64-
let result: any
64+
let result: any[] | undefined
6565
let marker: string | undefined
6666

6767
if (shouldReadCacheFromFetchPolicy(fetchPolicy)) {
@@ -91,14 +91,18 @@ async function _findMany<
9191
},
9292
})
9393

94+
const abort = store.$hooks.withAbort()
9495
await store.$hooks.callHook('fetchMany', {
9596
store,
9697
meta,
9798
collection,
9899
findOptions,
99-
getResult: () => result,
100-
setResult: (value) => {
100+
getResult: () => result ?? [],
101+
setResult: (value, options) => {
101102
result = value
103+
if (result?.length && options?.abort !== false) {
104+
abort()
105+
}
102106
},
103107
setMarker: (value) => {
104108
marker = value
@@ -121,25 +125,25 @@ async function _findMany<
121125
for (const item of result) {
122126
store.$processItemParsing(collection, item)
123127
}
124-
}
125128

126-
if (fetchPolicy !== 'no-cache') {
127-
const items = result
128-
const writes: Array<WriteItem<TCollection, TCollectionDefaults, TSchema>> = []
129-
for (const item of items) {
130-
const key = collection.getKey(item)
131-
if (!key) {
132-
console.warn(`Key is undefined for ${collection.name}. Item was not written to cache.`)
133-
continue
129+
if (fetchPolicy !== 'no-cache') {
130+
const items = result
131+
const writes: Array<WriteItem<TCollection, TCollectionDefaults, TSchema>> = []
132+
for (const item of items) {
133+
const key = collection.getKey(item)
134+
if (!key) {
135+
console.warn(`Key is undefined for ${collection.name}. Item was not written to cache.`)
136+
continue
137+
}
138+
writes.push({ key, value: item })
139+
}
140+
if (writes.length) {
141+
store.$cache.writeItems<TCollection>({
142+
collection,
143+
items: writes,
144+
marker: getMarker('many', marker),
145+
})
134146
}
135-
writes.push({ key, value: item })
136-
}
137-
if (writes.length) {
138-
store.$cache.writeItems<TCollection>({
139-
collection,
140-
items: writes,
141-
marker: getMarker('many', marker),
142-
})
143147
}
144148
}
145149
}
@@ -160,7 +164,7 @@ async function _findMany<
160164
}
161165

162166
return {
163-
result,
167+
result: result ?? [],
164168
marker,
165169
}
166170
}

packages/core/test/query/findFirst.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,88 @@ describe('findFirst', () => {
320320
])
321321
})
322322
})
323+
324+
describe('abort fetchFirst', () => {
325+
it('should abort fetchFirst if setResult is called', async () => {
326+
const fetchFirstHook1 = vi.fn((payload) => {
327+
payload.setResult({ id: '42' })
328+
})
329+
const fetchFirstHook2 = vi.fn((payload) => {
330+
payload.setResult({ id: '43' })
331+
})
332+
mockStore.$hooks.hook('fetchFirst', fetchFirstHook1)
333+
mockStore.$hooks.hook('fetchFirst', fetchFirstHook2)
334+
335+
const result = await findFirst({
336+
store: mockStore,
337+
collection,
338+
findOptions: '42',
339+
})
340+
341+
expect(result.result).toEqual({ id: '42' })
342+
expect(fetchFirstHook1).toHaveBeenCalled()
343+
expect(fetchFirstHook2).not.toHaveBeenCalled()
344+
})
345+
346+
it('should not abort if setResult is not called', async () => {
347+
const fetchFirstHook1 = vi.fn()
348+
const fetchFirstHook2 = vi.fn((payload) => {
349+
payload.setResult({ id: '43' })
350+
})
351+
mockStore.$hooks.hook('fetchFirst', fetchFirstHook1)
352+
mockStore.$hooks.hook('fetchFirst', fetchFirstHook2)
353+
354+
const result = await findFirst({
355+
store: mockStore,
356+
collection,
357+
findOptions: '42',
358+
})
359+
360+
expect(result.result).toEqual({ id: '43' })
361+
expect(fetchFirstHook1).toHaveBeenCalled()
362+
expect(fetchFirstHook2).toHaveBeenCalled()
363+
})
364+
365+
it('should not abort if setResult is called with abort: false', async () => {
366+
const fetchFirstHook1 = vi.fn((payload) => {
367+
payload.setResult({ id: '42' }, { abort: false })
368+
})
369+
const fetchFirstHook2 = vi.fn((payload) => {
370+
payload.setResult({ id: '43' })
371+
})
372+
mockStore.$hooks.hook('fetchFirst', fetchFirstHook1)
373+
mockStore.$hooks.hook('fetchFirst', fetchFirstHook2)
374+
375+
const result = await findFirst({
376+
store: mockStore,
377+
collection,
378+
findOptions: '42',
379+
})
380+
381+
expect(result.result).toEqual({ id: '43' })
382+
expect(fetchFirstHook1).toHaveBeenCalled()
383+
expect(fetchFirstHook2).toHaveBeenCalled()
384+
})
385+
386+
it('should not abort if result is nil', async () => {
387+
const fetchFirstHook1 = vi.fn((payload) => {
388+
payload.setResult(null)
389+
})
390+
const fetchFirstHook2 = vi.fn((payload) => {
391+
payload.setResult({ id: '43' })
392+
})
393+
mockStore.$hooks.hook('fetchFirst', fetchFirstHook1)
394+
mockStore.$hooks.hook('fetchFirst', fetchFirstHook2)
395+
396+
const result = await findFirst({
397+
store: mockStore,
398+
collection,
399+
findOptions: '42',
400+
})
401+
402+
expect(result.result).toEqual({ id: '43' })
403+
expect(fetchFirstHook1).toHaveBeenCalled()
404+
expect(fetchFirstHook2).toHaveBeenCalled()
405+
})
406+
})
323407
})

packages/core/test/query/findMany.spec.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,4 +266,109 @@ describe('findMany', () => {
266266
])
267267
})
268268
})
269+
270+
describe('abort fetchMany', () => {
271+
it('should abort fetchMany if setResult is called', async () => {
272+
const fetchManyHook1 = vi.fn((payload) => {
273+
payload.setResult([{ id: '42' }])
274+
})
275+
const fetchManyHook2 = vi.fn((payload) => {
276+
payload.setResult([{ id: '43' }])
277+
})
278+
mockStore.$hooks.hook('fetchMany', fetchManyHook1)
279+
mockStore.$hooks.hook('fetchMany', fetchManyHook2)
280+
281+
const result = await findMany({
282+
store: mockStore,
283+
collection,
284+
findOptions: { params: { teamId: '42' } },
285+
})
286+
287+
expect(result.result).toEqual([{ id: '42' }])
288+
expect(fetchManyHook1).toHaveBeenCalled()
289+
expect(fetchManyHook2).not.toHaveBeenCalled()
290+
})
291+
292+
it('should not abort if setResult is not called', async () => {
293+
const fetchManyHook1 = vi.fn()
294+
const fetchManyHook2 = vi.fn((payload) => {
295+
payload.setResult([{ id: '43' }])
296+
})
297+
mockStore.$hooks.hook('fetchMany', fetchManyHook1)
298+
mockStore.$hooks.hook('fetchMany', fetchManyHook2)
299+
300+
const result = await findMany({
301+
store: mockStore,
302+
collection,
303+
findOptions: { params: { teamId: '42' } },
304+
})
305+
306+
expect(result.result).toEqual([{ id: '43' }])
307+
expect(fetchManyHook1).toHaveBeenCalled()
308+
expect(fetchManyHook2).toHaveBeenCalled()
309+
})
310+
311+
it('should not abort if setResult is called with abort: false', async () => {
312+
const fetchManyHook1 = vi.fn((payload) => {
313+
payload.setResult([{ id: '42' }], { abort: false })
314+
})
315+
const fetchManyHook2 = vi.fn((payload) => {
316+
payload.setResult([{ id: '43' }])
317+
})
318+
mockStore.$hooks.hook('fetchMany', fetchManyHook1)
319+
mockStore.$hooks.hook('fetchMany', fetchManyHook2)
320+
321+
const result = await findMany({
322+
store: mockStore,
323+
collection,
324+
findOptions: { params: { teamId: '42' } },
325+
})
326+
327+
expect(result.result).toEqual([{ id: '43' }])
328+
expect(fetchManyHook1).toHaveBeenCalled()
329+
expect(fetchManyHook2).toHaveBeenCalled()
330+
})
331+
332+
it('should not abort if result is nil', async () => {
333+
const fetchManyHook1 = vi.fn((payload) => {
334+
payload.setResult(null)
335+
})
336+
const fetchManyHook2 = vi.fn((payload) => {
337+
payload.setResult([{ id: '43' }])
338+
})
339+
mockStore.$hooks.hook('fetchMany', fetchManyHook1)
340+
mockStore.$hooks.hook('fetchMany', fetchManyHook2)
341+
342+
const result = await findMany({
343+
store: mockStore,
344+
collection,
345+
findOptions: { params: { teamId: '42' } },
346+
})
347+
348+
expect(result.result).toEqual([{ id: '43' }])
349+
expect(fetchManyHook1).toHaveBeenCalled()
350+
expect(fetchManyHook2).toHaveBeenCalled()
351+
})
352+
353+
it('should not abort if result is empty array', async () => {
354+
const fetchManyHook1 = vi.fn((payload) => {
355+
payload.setResult([])
356+
})
357+
const fetchManyHook2 = vi.fn((payload) => {
358+
payload.setResult([{ id: '43' }])
359+
})
360+
mockStore.$hooks.hook('fetchMany', fetchManyHook1)
361+
mockStore.$hooks.hook('fetchMany', fetchManyHook2)
362+
363+
const result = await findMany({
364+
store: mockStore,
365+
collection,
366+
findOptions: { params: { teamId: '42' } },
367+
})
368+
369+
expect(result.result).toEqual([{ id: '43' }])
370+
expect(fetchManyHook1).toHaveBeenCalled()
371+
expect(fetchManyHook2).toHaveBeenCalled()
372+
})
373+
})
269374
})

packages/shared/src/types/hooks.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import type { Awaitable, Path, PathValue } from './utils'
99

1010
export interface CustomHookMeta {}
1111

12+
export interface AbortableOptions {
13+
/**
14+
* If true, the remaining hooks in the queue will not be called.
15+
* @default true
16+
*/
17+
abort?: boolean
18+
}
19+
1220
export interface HookDefinitions<
1321
TSchema extends StoreSchema,
1422
TCollectionDefaults extends CollectionDefaults,
@@ -62,7 +70,7 @@ export interface HookDefinitions<
6270
key?: string | number
6371
findOptions?: FindOptions<TCollection, TCollectionDefaults, TSchema>
6472
getResult: () => ResolvedCollectionItemBase<TCollection, TCollectionDefaults, TSchema> | undefined
65-
setResult: (result: ResolvedCollectionItemBase<TCollection, TCollectionDefaults, TSchema>) => void
73+
setResult: (result: ResolvedCollectionItemBase<TCollection, TCollectionDefaults, TSchema>, options?: AbortableOptions) => void
6674
setMarker: (marker: string) => void
6775
}
6876
) => Awaitable<void>
@@ -110,7 +118,7 @@ export interface HookDefinitions<
110118
collection: ResolvedCollection<TCollection, TCollectionDefaults, TSchema>
111119
findOptions?: FindOptions<TCollection, TCollectionDefaults, TSchema>
112120
getResult: () => Array<ResolvedCollectionItemBase<TCollection, TCollectionDefaults, TSchema>>
113-
setResult: (result: Array<ResolvedCollectionItemBase<TCollection, TCollectionDefaults, TSchema>>) => void
121+
setResult: (result: Array<ResolvedCollectionItemBase<TCollection, TCollectionDefaults, TSchema>>, options?: AbortableOptions) => void
114122
setMarker: (marker: string) => void
115123
}
116124
) => Awaitable<void>

0 commit comments

Comments
 (0)