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: customizable cache key #52

Merged
merged 4 commits into from
Sep 15, 2023
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
70 changes: 42 additions & 28 deletions docs/api/my-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,48 @@

Returns the raw response of the API endpoint. Intended for actions inside methods, e.g. when sending form data to the API when clicking a submit button.

## Type Declarations

```ts
interface BaseApiFetchOptions {
/**
* Skip the Nuxt server proxy and fetch directly from the API.
* Requires `allowClient` to be enabled in the module options as well.
* @default false
*/
client?: boolean
/**
* Cache the response for the same request.
* If set to `true`, the cache key will be generated from the request options.
* Alternatively, a custom cache key can be provided.
* @default false
*/
cache?: string | boolean
}

type ApiFetchOptions = Omit<NitroFetchOptions<string>, 'body' | 'cache'> & {
pathParams?: Record<string, string>
body?: string | Record<string, any> | FormData | null
}

function $Api<T = any>(
path: string,
opts?: ApiFetchOptions & BaseApiFetchOptions
): Promise<T>
```

## Caching

By default, a [unique key is generated](/guide/caching) based in input parameters for each request to ensure that data fetching can be properly de-duplicated across requests. You can provide a custom key by passing a string as the `cache` option:

```ts
const route = useRoute()

const data = await $myApi('posts', {
cache: `posts-${route.params.id}`
})
```

## Example

::: info
Expand Down Expand Up @@ -90,31 +132,3 @@ export default defineNuxtConfig({
}
})
```

## Type Declarations

```ts
interface BaseApiFetchOptions {
/**
* Skip the Nuxt server proxy and fetch directly from the API.
* Requires `allowClient` to be enabled in the module options as well.
* @default false
*/
client?: boolean
/**
* Cache the response for the same request
* @default false
*/
cache?: boolean
}

type ApiFetchOptions = Omit<NitroFetchOptions<string>, 'body' | 'cache'> & {
pathParams?: Record<string, string>
body?: string | Record<string, any> | FormData | null
}

type $Api = <T = any>(
path: string,
opts?: ApiFetchOptions & BaseApiFetchOptions
) => Promise<T>
```
110 changes: 65 additions & 45 deletions docs/api/use-my-api-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,71 @@ The composable supports every [`useAsyncData` option](https://nuxt.com/docs/api/

By default, Nuxt waits until a `refresh` is finished before it can be executed again. Passing `true` as parameter skips that wait.

## Type Declarations

```ts
type BaseUseApiDataOptions<T> = Omit<AsyncDataOptions<T>, 'watch'> & {
/**
* Skip the Nuxt server proxy and fetch directly from the API.
* Requires `allowClient` to be enabled in the module options as well.
* @default false
*/
client?: boolean
/**
* Cache the response for the same request.
* @default true
*/
cache?: boolean
/**
* Watch an array of reactive sources and auto-refresh the fetch result when they change.
* Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`.
*/
watch?: (WatchSource<unknown> | object)[] | false
}

type UseApiDataOptions<T> = Pick<
ComputedOptions<NitroFetchOptions<string>>,
| 'onRequest'
| 'onRequestError'
| 'onResponse'
| 'onResponseError'
| 'query'
| 'headers'
| 'method'
| 'retry'
| 'retryDelay'
| 'timeout'
> & {
pathParams?: MaybeRef<Record<string, string>>
body?: MaybeRef<string | Record<string, any> | FormData | null | undefined>
} & BaseUseApiDataOptions<T>

function UseApiData<T = any>(
path: MaybeRefOrGetter<string>,
opts?: UseApiDataOptions<T>
): AsyncData<T | undefined, FetchError>
function UseApiData<T = any>(
key: MaybeRefOrGetter<string>,
path: MaybeRefOrGetter<string>,
opts?: UseApiDataOptions<T>
): AsyncData<T | undefined, FetchError>
```

## Caching

By default, a [unique key is generated](/guide/caching) based in input parameters for each request to ensure that data fetching can be properly de-duplicated across requests. You can provide a custom key by passing a string as the first argument, just like the native [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data):

```ts
const route = useRoute()
const key = computed(() => `posts-${route.params.id}`)

const { data } = await useMyApiData(key, 'posts')
```

::: tip
The key can be a reactive value, e.g. a computed property.
:::

## Examples

::: info
Expand Down Expand Up @@ -124,48 +189,3 @@ export default defineNuxtConfig({
}
})
```

## Type Declarations

```ts
type BaseUseApiDataOptions<T> = Omit<AsyncDataOptions<T>, 'watch'> & {
/**
* Skip the Nuxt server proxy and fetch directly from the API.
* Requires `allowClient` to be enabled in the module options as well.
* @default false
*/
client?: boolean
/**
* Cache the response for the same request
* @default true
*/
cache?: boolean
/**
* Watch an array of reactive sources and auto-refresh the fetch result when they change.
* Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`.
*/
watch?: (WatchSource<unknown> | object)[] | false
}

type UseApiDataOptions<T> = Pick<
ComputedOptions<NitroFetchOptions<string>>,
| 'onRequest'
| 'onRequestError'
| 'onResponse'
| 'onResponseError'
| 'query'
| 'headers'
| 'method'
| 'retry'
| 'retryDelay'
| 'timeout'
> & {
pathParams?: MaybeRef<Record<string, string>>
body?: MaybeRef<string | Record<string, any> | FormData | null | undefined>
} & BaseUseApiDataOptions<T>

type UseApiData = <T = any>(
path: MaybeRefOrGetter<string>,
opts?: UseApiDataOptions<T>
) => AsyncData<T | undefined, FetchError>
```
50 changes: 28 additions & 22 deletions src/runtime/composables/$api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ export interface BaseApiFetchOptions {
*/
client?: boolean
/**
* Cache the response for the same request
* Cache the response for the same request.
* If set to `true`, the cache key will be generated from the request options.
* Alternatively, a custom cache key can be provided.
* @default false
*/
cache?: boolean
cache?: string | boolean
}

export type ApiFetchOptions = Omit<NitroFetchOptions<string>, 'body' | 'cache'> & {
Expand All @@ -40,7 +42,7 @@ export interface $OpenApi<Paths extends Record<string, PathItemObject>> {
path: P,
opts: BaseApiFetchOptions & Omit<OpenApiRequestOptions<Paths[`/${P}`]>, 'method'>
): Promise<OpenApiResponse<Paths[`/${P}`]['get']>>
<P extends AllPaths<Paths>, M extends IgnoreCase<keyof Paths[`/${P}`] & HttpMethod>>(
<P extends AllPaths<Paths>, M extends IgnoreCase<keyof Paths[`/${P}`] & HttpMethod>>(
path: P,
opts?: BaseApiFetchOptions & OpenApiRequestOptions<Paths[`/${P}`], M> & { method: M }
): Promise<OpenApiResponse<Paths[`/${P}`][Lowercase<M>]>>
Expand All @@ -52,7 +54,9 @@ export function _$api<T = any>(
opts: ApiFetchOptions & BaseApiFetchOptions = {},
): Promise<T> {
const nuxt = useNuxtApp()
const { apiParty } = useRuntimeConfig().public
const promiseMap = (nuxt._promiseMap = nuxt._promiseMap || new Map()) as Map<string, Promise<T>>

const {
pathParams,
query,
Expand All @@ -63,24 +67,26 @@ export function _$api<T = any>(
cache = false,
...fetchOptions
} = opts
const { apiParty } = useRuntimeConfig().public
const key = `$party${hash([
endpointId,
path,
pathParams,
query,
method,
...(isFormData(body) ? [] : [body]),
])}`

const _key = typeof cache === 'string'
? cache
: `$party${hash([
endpointId,
path,
pathParams,
query,
method,
...(isFormData(body) ? [] : [body]),
])}`

if (client && !apiParty.allowClient)
throw new Error('Client-side API requests are disabled. Set "allowClient: true" in the module options to enable them.')

if ((nuxt.isHydrating || cache) && key in nuxt.payload.data)
return Promise.resolve(nuxt.payload.data[key])
if ((nuxt.isHydrating || cache) && _key in nuxt.payload.data)
return Promise.resolve(nuxt.payload.data[_key])

if (promiseMap.has(key))
return promiseMap.get(key)!
if (promiseMap.has(_key))
return promiseMap.get(_key)!

const endpoints = (apiParty as unknown as ModuleOptions).endpoints || {}
const endpoint = endpoints[endpointId]
Expand Down Expand Up @@ -120,19 +126,19 @@ export function _$api<T = any>(
const request = (client ? clientFetcher() : serverFetcher())
.then((response) => {
if (process.server || cache)
nuxt.payload.data[key] = response
promiseMap.delete(key)
nuxt.payload.data[_key] = response
promiseMap.delete(_key)
return response
})
// Invalidate cache if request fails
.catch((error) => {
if (key in nuxt.payload.data)
delete nuxt.payload.data[key]
promiseMap.delete(key)
if (_key in nuxt.payload.data)
delete nuxt.payload.data[_key]
promiseMap.delete(_key)
throw error
}) as Promise<T>

promiseMap.set(key, request)
promiseMap.set(_key, request)

return request
}
Loading