Skip to content

Commit

Permalink
feat(plugins): add auto-refetch plugin (#97)
Browse files Browse the repository at this point in the history
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
3 people authored Jan 31, 2025
1 parent fb4c647 commit dcf8c57
Show file tree
Hide file tree
Showing 6 changed files with 417 additions and 0 deletions.
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": "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:^"
}
}
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

0 comments on commit dcf8c57

Please sign in to comment.