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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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(),
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
],
})
```

You can customize the refetch behavior individually for each query with the `autoRefetch` option:

```ts
useQuery({
key: ['todos'],
query: getTodos,
autoRefetch: false, // disable auto refetch
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
})
```

## 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": "echo \"Error: no test specified\" && exit 1"
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
},
"peerDependencies": {
"@pinia/colada": "workspace:^"
},
"devDependencies": {
"@pinia/colada": "workspace:^"
}
}
129 changes: 129 additions & 0 deletions plugins/auto-refetch/src/auto-refetch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* @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(pluginOptions)],
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
...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 disabled option globally', async () => {
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
const { query } = mountQuery(
{
staleTime: 1000,
},
{
enabled: 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('cleans up timeouts when query is unmounted', async () => {
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
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)
})
})
78 changes: 78 additions & 0 deletions plugins/auto-refetch/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { PiniaColadaPlugin } from '@pinia/colada'

export interface PiniaColadaAutoRefetchOptions {
/**
* Whether to enable auto refresh by default.
* @default true
*/
enabled?: boolean
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Plugin that automatically refreshes queries when they become stale
*/
export function PiniaColadaAutoRefetch(
options: PiniaColadaAutoRefetchOptions = {},
): PiniaColadaPlugin {
const { enabled = true } = options
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved

return ({ queryCache }) => {
// Keep track of active entries and their timeouts
const refetchTimeouts = new Map<string, NodeJS.Timeout>()

queryCache.$onAction(({ name, args, after }) => {
// We want refetch to happen only on the client
if (!import.meta.client) return

// Handle fetch to set up auto-refetch
if (name === 'refresh') {
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
const [entry] = args
const key = entry.key.join('/')

after(async () => {
// Skip if auto-refetch is disabled or if the query has no stale time
const queryEnabled = entry.options?.autoRefetch ?? enabled
const staleTime = entry.options?.staleTime
if (!queryEnabled || !staleTime || !entry.active) return

// Clear any existing timeout for this key
const existingTimeout = refetchTimeouts.get(key)
if (existingTimeout) {
clearTimeout(existingTimeout)
}

// Schedule next refetch
const timeout = setTimeout(() => {
if (entry.active && entry.options) {
queryCache.refresh(entry).catch(console.error)
refetchTimeouts.delete(key)
}
}, staleTime)

refetchTimeouts.set(key, timeout)
})
}

// Clean up timeouts when entry is removed
if (name === 'remove') {
const [entry] = args
const key = entry.key.join('/')
const timeout = refetchTimeouts.get(key)
if (timeout) {
clearTimeout(timeout)
refetchTimeouts.delete(key)
}
}
})
}
}

// Add types for the new option
declare module '@pinia/colada' {
interface UseQueryOptions {
/**
* Whether to automatically refresh this query when it becomes stale.
*/
autoRefetch?: boolean
ymansurozer marked this conversation as resolved.
Show resolved Hide resolved
}
}
19 changes: 19 additions & 0 deletions plugins/auto-refetch/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type Options, defineConfig } from 'tsup'

const commonOptions = {
// splitting: false,
sourcemap: true,
format: ['cjs', 'esm'],
external: ['vue', 'pinia', '@pinia/colada'],
dts: true,
target: 'esnext',
} satisfies Options

export default defineConfig([
{
...commonOptions,
clean: true,
entry: ['src/index.ts'],
globalName: 'PiniaColadaAutoRefetch',
},
])
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion scripts/release.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { dirname, join } from 'node:path'
Expand Down
Loading