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 5 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": "vitest --ui"
},
"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)
})
})
107 changes: 107 additions & 0 deletions plugins/auto-refetch/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { PiniaColadaPlugin, UseQueryOptions } from '@pinia/colada'
import type { MaybeRefOrGetter } from 'vue'
import { toValue } from 'vue'

export interface PiniaColadaAutoRefetchOptions {
/**
* Whether to enable auto refresh by default.
* @default false
*/
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 = false } = options

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

const createMapKey = (options: UseQueryOptions) => toValue(options.key).join('/')

const scheduleRefetch = (options: UseQueryOptions) => {
const key = createMapKey(options)

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

// Schedule next refetch
const timeout = setTimeout(() => {
if (options) {
const entry = queryCache.getEntries({ key: toValue(options.key) })?.[0]
if (entry) {
queryCache.refresh(entry).catch(console.error)
}
refetchTimeouts.delete(key)
}
}, options.staleTime)

refetchTimeouts.set(key, timeout)
}

/**
* Whether to schedule a refetch for the given entry
*/
const shouldScheduleRefetch = (options: UseQueryOptions) => {
const queryEnabled = options.autoRefetch ?? enabled
const staleTime = options.staleTime
return queryEnabled && staleTime
}

// Trigger a fetch on creation to enable auto-refetch on initial load
if (name === 'ensure') {
const [entry] = args
if (!shouldScheduleRefetch(entry)) return

scheduleRefetch(entry)
}

// Set up auto-refetch on every fetch
if (name === 'fetch') {
const [entry] = args

after(async () => {
if (!entry.options) return
if (!shouldScheduleRefetch(entry.options)) return

scheduleRefetch(entry.options)
})
}

// Clean up timeouts when entry is removed
if (name === 'remove') {
const [entry] = args
if (!entry.options) return

const key = createMapKey(entry.options)
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?: MaybeRefOrGetter<boolean>
}
}
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