# Endpoint Expiry Policy

By default, Reactive Data Client cache policy can be described as [stale-while-revalidate](https://web.dev/stale-while-revalidate/).
This means that when data is available it can avoid blocking the application by using the stale data. However, in the background
it will still refresh the data if old enough.

## Expiry status

### Fresh

Data in this state is considered new enough that it doesn't need to fetch.

### Stale

Data is still allowed to be shown, however Reactive Data Client might attempt to revalidate by fetching again.

[useSuspense()](https://dataclient.io/vue/api/useSuspense.md) considers fetching on mount as well as when its parameters change.
In these cases it will fetch if the data is considered stale.

### Invalid

Data should not be shown. Any components needing this data will trigger fetch
([mounted components keep their data](#invalidate) until it resolves). If no components care about this
data no action will be taken.

## Expiry Time

### Endpoint.dataExpiryLength

[Endpoint.dataExpiryLength](https://dataclient.io/rest/api/Endpoint.md#dataexpirylength) sets how long (in milliseconds) it takes for data
to transition from '[fresh](#fresh)' to '[stale](#stale)' status. Try setting it to a very low number like '50'
to make it becomes [stale](#stale) almost instantly; or a very large number to stay around for a long time.

Toggling between 'first' and 'second' changes the parameters. If the data is still considered fresh
you will continue to see the old time without any refresh.

```ts title="api/lastUpdated"
import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';

export class TimedEntity extends Entity {
  id = '';
  updatedAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    updatedAt: Temporal.Instant.from,
  };
}

export const lastUpdated = new RestEndpoint({
  path: '/api/currentTime/:id',
  schema: TimedEntity,
});
```

```ts title="getUpdated"
import { lastUpdated } from './api/lastUpdated';

export const getUpdated = lastUpdated.extend({ dataExpiryLength: 10000 });
```

```html title="TimePage.vue"
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { getUpdated } from './getUpdated';

  const props = defineProps<{ id: string }>();
  const time = await useSuspense(getUpdated, () => ({ id: props.id }));
</script>

<template>
  <div>
    API time for {{ id }}:
    <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
  </div>
</template>
```

```html title="Navigator.vue"
<script setup lang="ts">
  import { ref } from 'vue';
  import TimePage from './TimePage.vue';

  const id = ref('1');
</script>

<template>
  <div>
    <div>
      <button @click="id = '1'">First</button>
      <button @click="id = '2'">Second</button>
    </div>
    <!-- :key remounts TimePage so it suspends for the new id -->
    <Suspense timeout="0">
      <TimePage :key="id" :id="id" />
      <template #fallback><div>loading...</div></template>
    </Suspense>
  </div>
</template>
```

<details>

<summary>@data-client/rest</summary>

Long cache lifetime

```typescript title="LongLivingResource.ts"
import {
  RestEndpoint,
  RestGenerics,
  resource,
} from '@data-client/rest';

// We can now use LongLivingEndpoint to create endpoints that will be cached for one hour
class LongLivingEndpoint<
  O extends RestGenerics,
> extends RestEndpoint<O> {
  dataExpiryLength = 60 * 60 * 1000; // one hour
}

const LongLivingResource = resource({
  path: '/:id',
  Endpoint: LongLivingEndpoint,
});
```

Never retry on error

```typescript title="NoRetryResource.ts"
import {
  RestEndpoint,
  RestGenerics,
  resource,
} from '@data-client/rest';

// We can now use NoRetryEndpoint to create endpoints that will be cached for one hour
class NoRetryEndpoint<
  O extends RestGenerics,
> extends RestEndpoint<O> {
  errorExpiryLength = Infinity;
}

const NoRetryResource = resource({
  path: '/:id',
  Endpoint: NoRetryEndpoint,
});
```

</details>

### Endpoint.invalidIfStale

[Endpoint.invalidIfStale](https://dataclient.io/rest/api/Endpoint.md#invalidifstale) eliminates the '[stale](#stale)' status, making data
that expires immediately be considered '[invalid](#invalid)'.

This is demonstrated by the component suspending once its data goes stale. If the data is still
within the expiry time it just continues to display it.

```ts title="api/lastUpdated"
import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';

export class TimedEntity extends Entity {
  id = '';
  updatedAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    updatedAt: Temporal.Instant.from,
  };
}

export const lastUpdated = new RestEndpoint({
  path: '/api/currentTime/:id',
  schema: TimedEntity,
});
```

```ts title="getUpdated"
import { lastUpdated } from './api/lastUpdated';

export const getUpdated = lastUpdated.extend({
  invalidIfStale: true,
  dataExpiryLength: 5000,
});
```

```html title="TimePage.vue"
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { getUpdated } from './getUpdated';

  const props = defineProps<{ id: string }>();
  const time = await useSuspense(getUpdated, () => ({ id: props.id }));
</script>

<template>
  <div>
    API time for {{ id }}:
    <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
  </div>
</template>
```

```html title="Navigator.vue"
<script setup lang="ts">
  import { ref } from 'vue';
  import TimePage from './TimePage.vue';

  const id = ref('1');
</script>

<template>
  <div>
    <div>
      <button @click="id = '1'">First</button>
      <button @click="id = '2'">Second</button>
    </div>
    <!-- :key remounts TimePage so it suspends for the new id -->
    <Suspense timeout="0">
      <TimePage :key="id" :id="id" />
      <template #fallback><div>loading...</div></template>
    </Suspense>
  </div>
</template>
```

## Force refresh

We sometimes want to fetch new data; while continuing to show the old (stale) data.

### A specific endpoint

[Controller.fetch](https://dataclient.io/vue/api/Controller.md#fetch) can be used to trigger a fetch while still showing
the previous data. This can be done even with 'fresh' data.

```ts title="api/lastUpdated"
import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';

export class TimedEntity extends Entity {
  id = '';
  updatedAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    updatedAt: Temporal.Instant.from,
  };
}

export const lastUpdated = new RestEndpoint({
  path: '/api/currentTime/:id',
  schema: TimedEntity,
});
```

```html title="ShowTime.vue"
<script setup lang="ts">
  import { useController, useSuspense } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';

  const time = await useSuspense(lastUpdated, { id: '1' });
  const ctrl = useController();
</script>

<template>
  <div>
    <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
    <button @click="ctrl.fetch(lastUpdated, { id: '1' })">Refresh</button>
  </div>
</template>
```

### Refresh visible endpoints

[Controller.expireAll()](https://dataclient.io/vue/api/Controller.md#expireAll) sets all responses' [expiry status](#expiry-status) matching `testKey` to [Stale](#stale).

```ts title="api/lastUpdated"
import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';

export class TimedEntity extends Entity {
  id = '';
  updatedAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    updatedAt: Temporal.Instant.from,
  };
}

export const lastUpdated = new RestEndpoint({
  path: '/api/currentTime/:id',
  schema: TimedEntity,
});
```

```html title="ShowTime.vue"
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';

  const props = defineProps<{ id: string }>();
  const time = await useSuspense(lastUpdated, () => ({ id: props.id }));
</script>

<template>
  <div>
    <b>{{ id }}</b> <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
  </div>
</template>
```

```html title="Demo.vue"
<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';
  import ShowTime from './ShowTime.vue';

  const ctrl = useController();
</script>

<template>
  <div>
    <Suspense v-for="id in ['1', '2', '3']" :key="id">
      <ShowTime :id="id" />
      <template #fallback><div>{{ id }} Loading...</div></template>
    </Suspense>

    <button @click="ctrl.expireAll(lastUpdated)">Expire All</button>
    <button @click="ctrl.fetch(lastUpdated, { id: '1' })">
      Force Refresh First
    </button>
  </div>
</template>
```

## Invalidate {#invalidate}

Both [endpoints](https://dataclient.io/rest/api/Endpoint.md) and [entities](https://dataclient.io/rest/api/Entity.md) can be targetted to be invalidated.

Invalidated data always refetches, even when it is fresh. Vue can't suspend a component again once its
setup has run, so mounted components keep showing their previous data until the refetch resolves.
Meanwhile [useCache()](https://dataclient.io/vue/api/useCache.md) returns `undefined` and [useDLE()](https://dataclient.io/vue/api/useDLE.md)'s `loading`
is `true`. Components mounted after invalidation suspend until the new data arrives.

### A specific endpoint {#invalidate-endpoint}

In this example [invalidating the endpoint](https://dataclient.io/vue/api/Controller.md#invalidate) refetches it, even though its data is still fresh.

```ts title="api/lastUpdated"
import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';

export class TimedEntity extends Entity {
  id = '';
  updatedAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    updatedAt: Temporal.Instant.from,
  };
}

export const lastUpdated = new RestEndpoint({
  path: '/api/currentTime/:id',
  schema: TimedEntity,
});
```

```html title="ShowTime.vue"
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';

  const props = defineProps<{ id: string }>();
  const time = await useSuspense(lastUpdated, () => ({ id: props.id }));
</script>

<template>
  <div>
    <b>{{ id }}</b> <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
  </div>
</template>
```

```html title="Demo.vue"
<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';
  import ShowTime from './ShowTime.vue';

  const ctrl = useController();
</script>

<template>
  <div>
    <Suspense v-for="id in ['1', '2', '3']" :key="id">
      <ShowTime :id="id" />
      <template #fallback><div>{{ id }} Loading...</div></template>
    </Suspense>

    <button @click="ctrl.invalidateAll(lastUpdated)">Invalidate All</button>
    <button @click="ctrl.invalidate(lastUpdated, { id: '1' })">
      Invalidate First
    </button>
  </div>
</template>
```

### Any endpoint with an entity {#invalidate-entity}

Using the [Invalidate schema](https://dataclient.io/rest/api/Invalidate.md) allows us to invalidate _any_ endpoint that includes that relies on that [entity](https://dataclient.io/rest/api/Entity.md) in their
response. If the endpoint uses the entity in an [Array](https://dataclient.io/rest/api/Array.md), it will simply be removed from that [Array](https://dataclient.io/rest/api/Array.md).

```ts title="api/lastUpdated"
import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';

export class TimedEntity extends Entity {
  id = '';
  updatedAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    updatedAt: Temporal.Instant.from,
  };
}

export const lastUpdated = new RestEndpoint({
  path: '/api/currentTime/:id',
  schema: TimedEntity,
});
```

```html title="TimePage.vue"
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';

  const props = defineProps<{ id: string }>();
  const time = await useSuspense(lastUpdated, () => ({ id: props.id }));
</script>

<template>
  <div>
    API time for {{ id }}:
    <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
  </div>
</template>
```

```html title="ShowTime.vue"
<script setup lang="ts">
  import { Invalidate, RestEndpoint } from '@data-client/rest';
  import { useController, useLoading } from '@data-client/vue';
  import { TimedEntity } from './api/lastUpdated';
  import TimePage from './TimePage.vue';

  const InvalidateTimedEntity = new Invalidate(TimedEntity);
  const deleteLastUpdated = new RestEndpoint({
    path: '/api/currentTime/:id',
    method: 'DELETE',
    schema: InvalidateTimedEntity,
  });

  const ctrl = useController();
  const [handleDelete, loadingDelete] = useLoading(() =>
    ctrl.fetch(deleteLastUpdated, { id: '1' }),
  );
</script>

<template>
  <div>
    <Suspense>
      <TimePage id="1" />
      <template #fallback><div>loading...</div></template>
    </Suspense>
    <button @click="handleDelete">
      {{ loadingDelete ? 'loading...' : 'Invalidate' }}
    </button>
    <button
      @click="ctrl.setResponse(deleteLastUpdated, { id: '1' }, { id: '1' })"
    >
      Invalidate (without fetching DELETE)
    </button>
    <button @click="ctrl.set([InvalidateTimedEntity], [{ id: '1' }])">
      Invalidate Entity with ctrl.set
    </button>
  </div>
</template>
```

[Controller.fetch()](https://dataclient.io/vue/api/Controller.md#fetch) lets us update the server and store.
We can use [Controller.setResponse()](https://dataclient.io/vue/api/Controller.md#setResponse) or [Controller.set()](https://dataclient.io/vue/api/Controller.md#set)
when we want to change the local store directly.

#### Conditional Invalidation based on data

If `invalidation` should happen only sometimes, based on the response data, we can
return `undefined` from [Entity.process](https://dataclient.io/rest/api/Entity.md#process).

```ts
class PriceLevel extends Entity {
  price = 0;
  amount = 0;

  pk() {
    return this.price;
  }

  static process(
    input: [number, number],
    parent: any,
    key: string | undefined,
  ): any {
    const [price, amount] = input;
    if (amount === 0) return undefined;
    return { price, amount };
  }
}
```
