Skip to content

Commit

Permalink
feat: customizable cache key (#52)
Browse files Browse the repository at this point in the history
* feat: customizable async data key

* feat: `key` support for `$api`

* refactor: simplify cache option

* docs: update
  • Loading branch information
johannschopplich authored Sep 15, 2023
1 parent 4d22106 commit a1ee25e
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 114 deletions.
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

0 comments on commit a1ee25e

Please sign in to comment.