<script setup lang="ts">
import { TanStackDevtools } from '@tanstack/vue-devtools'
import { pacerDevtoolsPlugin } from '@tanstack/vue-pacer-devtools'
import { ref, onUnmounted } from 'vue'
import { useAsyncRateLimiter } from '@tanstack/vue-pacer/async-rate-limiter'
const dev = import.meta.env.DEV
const plugins = [pacerDevtoolsPlugin()]
interface SearchResult {
id: number
title: string
}
const fakeApi = async (term: string): Promise<Array<SearchResult>> => {
await new Promise((resolve) => setTimeout(resolve, 300))
return [
{ id: 1, title: `${term} result ${Math.floor(Math.random() * 100)}` },
{ id: 2, title: `${term} result ${Math.floor(Math.random() * 100)}` },
{ id: 3, title: `${term} result ${Math.floor(Math.random() * 100)}` },
]
}
const windowType = ref<'fixed' | 'sliding'>('fixed')
const searchTerm = ref('')
const results = ref<Array<SearchResult>>([])
const error = ref<Error | null>(null)
const handleSearch = async (term: string) => {
if (!term) {
results.value = []
return
}
const data = await fakeApi(term)
results.value = data
error.value = null
}
const setSearchAsyncRateLimiter = useAsyncRateLimiter(handleSearch, () => ({
key: 'useAsyncRateLimiter',
windowType: windowType.value,
limit: 3,
window: 3000,
onReject: (_args, rateLimiter) => {
console.log(
`Rate limit reached. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`,
)
},
onError: (cause) => {
console.error('Search failed:', cause)
error.value = cause as Error
results.value = []
},
}))
const handleSearchRateLimited = setSearchAsyncRateLimiter.maybeExecute
onUnmounted(() => {
console.log('unmount')
setSearchAsyncRateLimiter.reset()
})
async function onSearchChange(e: Event) {
const newTerm = (e.target as HTMLInputElement).value
searchTerm.value = newTerm
await handleSearchRateLimited(newTerm)
}
</script>
<template>
<div>
<h1>TanStack Pacer useAsyncRateLimiter Example</h1>
<div :style="{ display: 'grid', gap: '0.5rem', marginBottom: '1rem' }">
<label
><input
type="radio"
name="windowType"
value="fixed"
:checked="windowType === 'fixed'"
@input="() => (windowType = 'fixed')"
/>Fixed Window</label
><label
><input
type="radio"
name="windowType"
value="sliding"
:checked="windowType === 'sliding'"
@input="() => (windowType = 'sliding')"
/>Sliding Window</label
>
</div>
<div>
<input
autofocus
type="search"
:value="searchTerm"
@input="onSearchChange"
placeholder="Type to search..."
:style="{ width: '100%' }"
autocomplete="new-password"
/>
</div>
<template v-if="error"
><div>Error: {{ error.message }}</div></template
><setSearchAsyncRateLimiter.Subscribe
:selector="
(state) => ({
successCount: state.successCount,
rejectionCount: state.rejectionCount,
isExecuting: state.isExecuting,
})
"
v-slot="{ successCount, rejectionCount, isExecuting }"
><div>
<table>
<tbody>
<tr>
<td>API calls made:</td>
<td>{{ successCount }}</td>
</tr>
<tr>
<td>Rejected calls:</td>
<td>{{ rejectionCount }}</td>
</tr>
<tr>
<td>Is executing:</td>
<td>{{ isExecuting ? 'Yes' : 'No' }}</td>
</tr>
<tr>
<td>Results:</td>
<td>
<template v-if="results.length > 0"
><ul>
<template v-for="(item, index) in results" :key="index"
><li>{{ item.title }}</li></template
>
</ul></template
><template v-else>{{ 'No results' }}</template>
</td>
</tr>
</tbody>
</table>
</div></setSearchAsyncRateLimiter.Subscribe
><setSearchAsyncRateLimiter.Subscribe
:selector="(state) => state"
v-slot="state"
>
<pre :style="{ marginTop: '20px' }">{{
JSON.stringify(state, null, 2)
}}</pre>
</setSearchAsyncRateLimiter.Subscribe>
</div>
<TanStackDevtools v-if="dev" :plugins="plugins" />
</template>