-
-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(plugins): add auto-refetch plugin (#97)
Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> Co-authored-by: Eduardo San Martin Morote <posva13@gmail.com>
- Loading branch information
1 parent
fb4c647
commit dcf8c57
Showing
6 changed files
with
417 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<h1> | ||
<img height="76" src="https://github.com/posva/pinia-colada/assets/664177/02011637-f94d-4a35-854a-02f7aed86a3c" alt="Pinia Colada logo"> | ||
Pinia Colada Auto Refetch | ||
</h1> | ||
|
||
<a href="https://npmjs.com/package/@pinia/colada-plugin-auto-refetch"> | ||
<img src="https://badgen.net/npm/v/@pinia/colada-plugin-auto-refetch/latest" alt="npm package"> | ||
</a> | ||
|
||
Automatically refetch queries when they become stale in Pinia Colada. | ||
|
||
## Installation | ||
|
||
```sh | ||
npm install @pinia/colada-plugin-auto-refetch | ||
``` | ||
|
||
## Usage | ||
|
||
```js | ||
import { PiniaColadaAutoRefetch } from '@pinia/colada-plugin-auto-refetch' | ||
|
||
// Pass the plugin to Pinia Colada options | ||
app.use(PiniaColada, { | ||
// ... | ||
plugins: [ | ||
PiniaColadaAutoRefetch({ autoRefetch: true }), // enable globally | ||
], | ||
}) | ||
``` | ||
|
||
You can customize the refetch behavior individually for each query with the `autoRefetch` option: | ||
|
||
```ts | ||
useQuery({ | ||
key: ['todos'], | ||
query: getTodos, | ||
autoRefetch: true, // override local autoRefetch | ||
}) | ||
``` | ||
|
||
## License | ||
|
||
[MIT](http://opensource.org/licenses/MIT) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
{ | ||
"name": "@pinia/colada-plugin-auto-refetch", | ||
"type": "module", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"version": "0.0.1", | ||
"description": "Automatically refetch queries when they become stale in Pinia Colada", | ||
"author": { | ||
"name": "Yusuf Mansur Ozer", | ||
"email": "ymansurozer@gmail.com" | ||
}, | ||
"license": "MIT", | ||
"homepage": "https://github.com/posva/pinia-colada/plugins/auto-refetch#readme", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/posva/pinia-colada.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/posva/pinia-colada/issues" | ||
}, | ||
"keywords": [ | ||
"pinia", | ||
"plugin", | ||
"data", | ||
"fetching", | ||
"query", | ||
"mutation", | ||
"cache", | ||
"layer", | ||
"refetch" | ||
], | ||
"sideEffects": false, | ||
"exports": { | ||
".": { | ||
"types": { | ||
"import": "./dist/index.d.ts", | ||
"require": "./dist/index.d.cts" | ||
}, | ||
"import": "./dist/index.js", | ||
"require": "./dist/index.cjs" | ||
} | ||
}, | ||
"main": "./dist/index.cjs", | ||
"module": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"typesVersions": { | ||
"*": { | ||
"*": [ | ||
"./dist/*", | ||
"./*" | ||
] | ||
} | ||
}, | ||
"files": [ | ||
"LICENSE", | ||
"README.md", | ||
"dist" | ||
], | ||
"scripts": { | ||
"build": "tsup", | ||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -l @pinia/colada-plugin-auto-refetch -r 1", | ||
"test": "vitest --ui" | ||
}, | ||
"peerDependencies": { | ||
"@pinia/colada": "workspace:^" | ||
}, | ||
"devDependencies": { | ||
"@pinia/colada": "workspace:^" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
/** | ||
* @vitest-environment happy-dom | ||
*/ | ||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' | ||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' | ||
import { defineComponent } from 'vue' | ||
import { createPinia } from 'pinia' | ||
import { useQuery, PiniaColada } from '@pinia/colada' | ||
import type { UseQueryOptions } from '@pinia/colada' | ||
import type { PiniaColadaAutoRefetchOptions } from '.' | ||
import { PiniaColadaAutoRefetch } from '.' | ||
|
||
describe('Auto Refetch plugin', () => { | ||
beforeEach(() => { | ||
vi.clearAllTimers() | ||
vi.useFakeTimers() | ||
}) | ||
|
||
afterEach(() => { | ||
vi.restoreAllMocks() | ||
}) | ||
|
||
enableAutoUnmount(afterEach) | ||
|
||
function mountQuery( | ||
queryOptions?: Partial<UseQueryOptions>, | ||
pluginOptions?: PiniaColadaAutoRefetchOptions, | ||
) { | ||
const query = vi.fn(async () => 'result') | ||
const wrapper = mount( | ||
defineComponent({ | ||
template: '<div></div>', | ||
setup() { | ||
return useQuery({ | ||
query, | ||
key: ['test'], | ||
...queryOptions, | ||
}) | ||
}, | ||
}), | ||
{ | ||
global: { | ||
plugins: [ | ||
createPinia(), | ||
[ | ||
PiniaColada, | ||
{ | ||
plugins: [PiniaColadaAutoRefetch({ autoRefetch: true, ...pluginOptions })], | ||
...pluginOptions, | ||
}, | ||
], | ||
], | ||
}, | ||
}, | ||
) | ||
|
||
return { wrapper, query } | ||
} | ||
|
||
it('automatically refetches when stale time is reached', async () => { | ||
const { query } = mountQuery({ | ||
staleTime: 1000, | ||
}) | ||
|
||
// Wait for initial query | ||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
|
||
// Advance time past stale time in one go | ||
vi.advanceTimersByTime(1000) | ||
await flushPromises() | ||
|
||
expect(query).toHaveBeenCalledTimes(2) | ||
}) | ||
|
||
it('respects enabled option globally', async () => { | ||
const { query } = mountQuery( | ||
{ | ||
staleTime: 1000, | ||
}, | ||
{ | ||
autoRefetch: false, | ||
}, | ||
) | ||
|
||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
|
||
vi.advanceTimersByTime(2000) | ||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('respects disabled option per query', async () => { | ||
const { query } = mountQuery({ | ||
staleTime: 1000, | ||
autoRefetch: false, | ||
}) | ||
|
||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
|
||
vi.advanceTimersByTime(2000) | ||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('avoids refetching an unactive query', async () => { | ||
const { wrapper, query } = mountQuery({ | ||
staleTime: 1000, | ||
}) | ||
|
||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
|
||
wrapper.unmount() | ||
vi.advanceTimersByTime(2000) | ||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('does not refetch when staleTime is not set', async () => { | ||
const { query } = mountQuery({}) | ||
|
||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
|
||
vi.advanceTimersByTime(2000) | ||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('resets the stale timer when a new request occurs', async () => { | ||
const { query, wrapper } = mountQuery({ | ||
staleTime: 1000, | ||
}) | ||
|
||
// Wait for initial query | ||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(1) | ||
|
||
// Advance time partially (500ms) | ||
vi.advanceTimersByTime(500) | ||
|
||
// Manually trigger a new request | ||
query.mockImplementationOnce(async () => 'new result') | ||
await wrapper.vm.refetch() | ||
await flushPromises() | ||
expect(query).toHaveBeenCalledTimes(2) | ||
expect(wrapper.vm.data).toBe('new result') | ||
|
||
// Advance time to surpass the original stale time | ||
vi.advanceTimersByTime(700) | ||
await flushPromises() | ||
// Should not have triggered another request yet | ||
expect(query).toHaveBeenCalledTimes(2) | ||
|
||
// Advance to the new stale time (500ms more to reach full 1000ms from last request) | ||
vi.advanceTimersByTime(500) | ||
await flushPromises() | ||
// Now it should have triggered another request | ||
expect(query).toHaveBeenCalledTimes(3) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import type { PiniaColadaPlugin, UseQueryEntry, UseQueryOptions } from '@pinia/colada' | ||
import { toValue } from 'vue' | ||
|
||
/** | ||
* @module @pinia/colada-plugin-auto-refetch | ||
*/ | ||
|
||
/** | ||
* Options for the auto-refetch plugin. | ||
*/ | ||
export interface PiniaColadaAutoRefetchOptions { | ||
/** | ||
* Whether to enable auto refresh by default. | ||
* @default false | ||
*/ | ||
autoRefetch?: boolean | ||
} | ||
|
||
/** | ||
* To store timeouts in the entry extensions. | ||
*/ | ||
const refetchTimeoutKey = Symbol() | ||
|
||
/** | ||
* Plugin that automatically refreshes queries when they become stale | ||
*/ | ||
export function PiniaColadaAutoRefetch( | ||
options: PiniaColadaAutoRefetchOptions = {}, | ||
): PiniaColadaPlugin { | ||
const { autoRefetch = false } = options | ||
|
||
return ({ queryCache }) => { | ||
// Skip setting auto-refetch on the server | ||
if (typeof document === 'undefined') return | ||
|
||
function scheduleRefetch(entry: UseQueryEntry, options: UseQueryOptions) { | ||
if (!entry.active) return | ||
|
||
// Always clear existing timeout first | ||
clearTimeout(entry.ext[refetchTimeoutKey]) | ||
|
||
// Schedule next refetch | ||
const timeout = setTimeout(() => { | ||
if (options) { | ||
const entry: UseQueryEntry | undefined = queryCache.getEntries({ | ||
key: toValue(options.key), | ||
})?.[0] | ||
if (entry && entry.active) { | ||
queryCache.refresh(entry).catch(console.error) | ||
} | ||
} | ||
}, options.staleTime) | ||
|
||
entry.ext[refetchTimeoutKey] = timeout | ||
} | ||
|
||
queryCache.$onAction(({ name, args, after }) => { | ||
/** | ||
* Whether to schedule a refetch for the given entry | ||
*/ | ||
function shouldScheduleRefetch(options: UseQueryOptions) { | ||
const queryEnabled = toValue(options.autoRefetch) ?? autoRefetch | ||
const staleTime = options.staleTime | ||
return Boolean(queryEnabled && staleTime) | ||
} | ||
|
||
// Trigger a fetch on creation to enable auto-refetch on initial load | ||
if (name === 'ensure') { | ||
const [options] = args | ||
after((entry) => { | ||
if (!shouldScheduleRefetch(options)) return | ||
scheduleRefetch(entry, options) | ||
}) | ||
} | ||
|
||
// Set up auto-refetch on every fetch | ||
if (name === 'fetch') { | ||
const [entry] = args | ||
|
||
// Clear any existing timeout before scheduling a new one | ||
clearTimeout(entry.ext[refetchTimeoutKey]) | ||
|
||
after(async () => { | ||
if (!entry.options) return | ||
if (!shouldScheduleRefetch(entry.options)) return | ||
|
||
scheduleRefetch(entry, entry.options) | ||
}) | ||
} | ||
|
||
// Clean up timeouts when entry is removed | ||
if (name === 'remove') { | ||
const [entry] = args | ||
clearTimeout(entry.ext[refetchTimeoutKey]) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// Add types for the new option | ||
declare module '@pinia/colada' { | ||
// eslint-disable-next-line unused-imports/no-unused-vars | ||
interface UseQueryOptions<TResult, TError> extends PiniaColadaAutoRefetchOptions {} | ||
|
||
// eslint-disable-next-line unused-imports/no-unused-vars | ||
interface UseQueryEntryExtensions<TResult, TError> { | ||
/** | ||
* Used to store the timeout for the auto-refetch plugin. | ||
* @internal | ||
*/ | ||
[refetchTimeoutKey]?: ReturnType<typeof setTimeout> | ||
} | ||
} |
Oops, something went wrong.