Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugins): add auto-refetch plugin #97

Merged
merged 14 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions plugins/auto-refetch/README.md
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)
71 changes: 71 additions & 0 deletions plugins/auto-refetch/package.json
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": "[email protected]"
},
"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:^"
}
}
164 changes: 164 additions & 0 deletions plugins/auto-refetch/src/auto-refetch.spec.ts
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)
})
})
113 changes: 113 additions & 0 deletions plugins/auto-refetch/src/index.ts
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>
}
}
Loading