# Controller

`Controller` is a singleton providing safe access to the Reactive Data Client [flux store and lifecycle](https://dataclient.io/vue/api/Manager.md#control-flow).
`Controller` memoizes all store access, allowing a global referential equality guarantee and the fastest rendering
and retrieval performance.

`Controller` is provided:

- [Managers](https://dataclient.io/vue/api/Manager.md) as the first argument in [Manager.middleware](https://dataclient.io/vue/api/Manager.md#middleware)
- Vue with [useController()](https://dataclient.io/vue/api/useController.md)
- [Unit testing composables](https://dataclient.io/vue/guides/unit-testing-composables.md) with `renderDataCompose()` from `@data-client/vue/test`

```ts
class Controller {
  /*************** Action Dispatchers ***************/
  fetch(endpoint, ...args): ReturnType<E>;
  fetchIfStale(endpoint, ...args): ReturnType<E> | undefined;
  expireAll({ testKey }): Promise<void>;
  invalidate(endpoint, ...args): Promise<void>;
  invalidateAll({ testKey }): Promise<void>;
  resetEntireStore(): Promise<void>;
  set(queryable, ...args, value): Promise<void>;
  set([Entity], rows): Promise<void>;
  setResponse(endpoint, ...args, response): Promise<void>;
  setError(endpoint, ...args, error): Promise<void>;
  resolve(endpoint, { args, response, fetchedAt, error }): Promise<void>;
  subscribe(endpoint, ...args): Promise<void>;
  unsubscribe(endpoint, ...args): Promise<void>;
  /*************** Data Access ***************/
  get(queryable, ...args, state): Denormalized<typeof queryable>;
  getResponse(endpoint, ...args, state): { data; expiryStatus; expiresAt };
  getError(endpoint, ...args, state): ErrorTypes | undefined;
  snapshot(state: State<unknown>, fetchedAt?: number): SnapshotInterface;
  getState(): State<unknown>;
}
```

## Action Dispatchers

### fetch(endpoint, ...args) {#fetch}

Fetches the endpoint with given args, updating the Reactive Data Client cache with
the response or error upon completion.

**Create**

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

  const ctrl = useController();

  const handleSubmit = (e: Event) =>
    ctrl.fetch(
      PostResource.getList.push,
      new FormData(e.target as HTMLFormElement),
    );
</script>

<template>
  <form @submit.prevent="handleSubmit"><!-- ... --></form>
</template>
```

**Update**

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

  const props = defineProps<{ id: string }>();
  const ctrl = useController();

  const handleSubmit = (e: Event) =>
    ctrl.fetch(
      PostResource.update,
      { id: props.id },
      new FormData(e.target as HTMLFormElement),
    );
</script>

<template>
  <form @submit.prevent="handleSubmit"><!-- ... --></form>
</template>
```

**Delete**

```html title="PostListItem.vue"
<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import { useRouter } from 'vue-router';
  import { Post, PostResource } from './PostResource';

  const props = defineProps<{ post: Post }>();
  const ctrl = useController();
  const router = useRouter();

  const handleDelete = async () => {
    await ctrl.fetch(PostResource.delete, { id: props.post.id });
    router.push('/');
  };
</script>

<template>
  <div>
    <h3>{{ post.title }}</h3>
    <button @click="handleDelete">X</button>
  </div>
</template>
```

> **Tip**
>
> `fetch` has the same return value as the [Endpoint](https://dataclient.io/rest/api/Endpoint.md) passed to it.
> When using schemas, the denormalized value is returned
>
> ```ts
> const controller = useController();
>
> const post = await controller.fetch(
>   PostResource.getList.push,
>   createPayload,
> );
> post.title;
> post.pk();
> ```

#### Endpoint.sideEffect

[sideEffect](https://dataclient.io/rest/api/Endpoint.md#sideeffect) changes the behavior

##### true

- Resolves _before_ [committing](https://react.dev/learn/render-and-commit#step-3-react-commits-changes-to-the-dom) Reactive Data Client cache updates. (React 16, 17)
- Each call will always cause a new fetch.

##### false | undefined

- Resolves _after_ [committing](https://react.dev/learn/render-and-commit#step-3-react-commits-changes-to-the-dom) Reactive Data Client cache updates.
- Identical requests are deduplicated globally; allowing only one inflight request at a time.
  - To ensure a _new_ request is started, make sure to abort any existing inflight requests.

### fetchIfStale(endpoint, ...args) {#fetchIfStale}

Fetches only if endpoint is considered '[stale](https://dataclient.io/vue/concepts/expiry-policy.md#stale)'.

This can be useful when prefetching data, as it avoids overfetching fresh data.

An [example](https://stackblitz.com/github/reactive/data-client/tree/master/examples/github-app?file=src%2Frouting%2Froutes.tsx) with a fetch-as-you-render router:

```ts
{
  name: 'IssueList',
  component: lazyPage('IssuesPage'),
  title: 'issue list',
  resolveData: async (
    controller: Controller,
    { owner, repo }: { owner: string; repo: string },
    searchParams: URLSearchParams,
  ) => {
    const q = searchParams?.get('q') || 'is:issue is:open';
    await controller.fetchIfStale(IssueResource.search, {
      owner,
      repo,
      q,
    });
  },
},
```

### expireAll({ testKey }) {#expireAll}

Sets all responses' [expiry status](https://dataclient.io/vue/concepts/expiry-policy.md) matching `testKey` to [Stale](https://dataclient.io/vue/concepts/expiry-policy.md#stale).

This is sometimes useful to trigger refresh of only data presently shown
when there are many parameterizations in cache.

```html title="CreateTrade.vue"
<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import { AccountResource, TradeResource, type Trade } from './resources';
  import TradeForm from './TradeForm.vue';

  const props = defineProps<{ userId: string }>();
  const ctrl = useController();

  const handleTrade = async (trade: Trade) => {
    await ctrl.fetch(
      TradeResource.getList.push,
      { user: props.userId },
      trade,
    );
    ctrl.expireAll(AccountResource.get);
    ctrl.expireAll(AccountResource.getList);
  };
</script>

<template>
  <TradeForm @submit="handleTrade" />
</template>
```

> **Tip**
>
> To reduce load, improve performance, and improve state consistency; it can often be
> better to [include mutation sideeffects in the mutation response](https://dataclient.io/rest/guides/side-effects.md).

### invalidate(endpoint, ...args) {#invalidate}

Forces refetching on [useSuspense](https://dataclient.io/vue/api/useSuspense.md) with the same Endpoint
and parameters. Mounted components [keep showing their current data](https://dataclient.io/vue/concepts/expiry-policy.md#invalidate)
until the refetch resolves.

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

  const props = defineProps<{ id: string }>();
  const ctrl = useController();
  const article = await useSuspense(ArticleResource.get, () => ({
    id: props.id,
  }));
</script>

<template>
  <div>
    <h1>{{ article.title }}</h1>
    <button @click="ctrl.invalidate(ArticleResource.get, { id })">
      Refetch
    </button>
  </div>
</template>
```

> **Tip: Invalidate many endpoints at once**
>
> Use [schema.Invalidate](https://dataclient.io/rest/api/Invalidate.md) to invalidate every endpoint that contains a given entity.
>
> For REST try using [Resource.delete](https://dataclient.io/rest/api/resource.md#delete)
>
> ```ts
> // deletes MyResource(5)
> // this will refetch MyResource.get({id: '5'})
> // and remove it from MyResource.getList
> controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' });
> ```

### invalidateAll({ testKey }) {#invalidateAll}

[Invalidates](https://dataclient.io/vue/concepts/expiry-policy.md#invalid) all [endpoint keys](https://dataclient.io/rest/api/RestEndpoint.md#key) matching `testKey`.

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

  const props = defineProps<{ id: string }>();
  const ctrl = useController();
  const article = await useSuspense(ArticleResource.get, () => ({
    id: props.id,
  }));
</script>

<template>
  <div>
    <h1>{{ article.title }}</h1>
    <button @click="ctrl.invalidateAll(ArticleResource.get)">
      Refetch
    </button>
  </div>
</template>
```

Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache.

```ts
const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);

function useLogout() {
  const ctrl = useController();
  return () => ctrl.invalidateAll({ testKey });
}
```

It's usually a good idea to also clear cache on 401 (unauthorized) with [LogoutManager](https://dataclient.io/vue/api/LogoutManager.md)
as well.

```ts title="main.ts"
import { createApp } from 'vue';
import {
  DataClientPlugin,
  LogoutManager,
  getDefaultManagers,
} from '@data-client/vue';
import App from './App.vue';
import { unAuth } from '../authentication';

const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);

const managers = [
  new LogoutManager({
    handleLogout(controller) {
      // call custom unAuth function we defined
      unAuth();
      // still reset the store
      controller.invalidateAll({ testKey });
    },
  }),
  ...getDefaultManagers(),
];

const app = createApp(App);
app.use(DataClientPlugin, { managers });
app.mount('#app');
```

### resetEntireStore() {#resetEntireStore}

Resets/clears the entire Reactive Data Client cache. All inflight requests will not resolve.

This is typically used when logging out or changing authenticated users.

```html title="UserName.vue"
<script setup lang="ts">
  import { useController, useSuspense } from '@data-client/vue';
  import { CurrentUserResource } from './CurrentUserResource';
  import { impersonateUser } from './auth';

  const USER_NUMBER_ONE: string = '1111';

  const user = await useSuspense(CurrentUserResource.get);
  const ctrl = useController();

  const becomeAdmin = () => {
    // Changes the current user
    impersonateUser(USER_NUMBER_ONE);
    ctrl.resetEntireStore();
  };
</script>

<template>
  <div>
    <h1>{{ user.name }}</h1>
    <button @click="becomeAdmin">Be Number One</button>
  </div>
</template>
```

### set(queryable, ...args, value) {#set}

Updates any [Queryable](https://dataclient.io/rest/api/schema.md#queryable) [Schema](https://dataclient.io/rest/api/schema.md#schema-overview), or many entities at once with an [Array](https://dataclient.io/rest/api/Array.md) or [Values](https://dataclient.io/rest/api/Values.md) schema.

```ts
ctrl.set(
  Todo,
  // which Todo to update
  { id: '5' },
  // merge this data into the Todo in the store
  { id: '5', title: 'tell me friends how great Data Client is' },
);
```

The value is typed by the schema: an [Entity](https://dataclient.io/rest/api/Entity.md) takes its fields (numbers and strings may be either),
while a [Collection](https://dataclient.io/rest/api/Collection.md) or [All](https://dataclient.io/rest/api/All.md) takes a list of rows. A [Query](https://dataclient.io/rest/api/Query.md)
takes the input of the schema it wraps, since `set()` normalizes that schema rather than reversing `process()`.

```ts
ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]);
```

> **Note: Unions**
>
> When each member declares its discriminator as a literal (like `readonly type = 'first'`), a
> [Union](https://dataclient.io/rest/api/Union.md) row is checked against the member it selects, so `{ type: 'first', secondField: 1 }` is an
> error. Only declared fields are accepted, so a key read by a
> `schemaAttribute` function must be declared on each member.

Functions can be used in the value when derived data is used. This [prevents race conditions](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state).

```ts
const id = '2';
ctrl.set(Article, { id }, article => ({ id, votes: article.votes + 1 }));
```

#### set(\[Entity], rows) {#set-array}

Pass an [Array](https://dataclient.io/rest/api/Array.md) schema (`[Todo]` or `new schema.Array(Todo)`) and a list of rows to update
many entities in one store update. Each row merges with its stored entity; entities not in the list are untouched.

```ts
ctrl.set(
  [Todo],
  [
    { id: '5', completed: true },
    { id: '6', completed: false },
  ],
);
```

Rows are typed by the Entity's fields; numbers and strings may be either, and object, array and Date values are not
checked since rows are raw input.

For lists that mix Entity types, use a [Union](https://dataclient.io/rest/api/Union.md); each row is stored by its `type`:

```ts
const Feed = new schema.Union({ post: Post, comment: Comment }, 'type');

ctrl.set(
  [Feed],
  [
    { id: '1', type: 'post', title: 'Hello' },
    { id: '7', type: 'comment', body: 'Nice!' },
  ],
);
```

To delete many entities at once, use [Invalidate](https://dataclient.io/rest/api/Invalidate.md#batch-invalidation); rows only need their pk
fields:

```ts
ctrl.set([new schema.Invalidate(Todo)], [{ id: '5' }, { id: '6' }]);
```

To delete one, pass the Invalidate schema and its row:

```ts
ctrl.set(new schema.Invalidate(Todo), { id: '5' });
```

[Values](https://dataclient.io/rest/api/Values.md) schemas take an object of rows instead:

```ts
ctrl.set(new schema.Values(Todo), {
  '5': { id: '5', completed: true },
  '6': { id: '6', completed: false },
});
```

Array, Values and Invalidate schemas take no `args` (so [Entity.pk()](https://dataclient.io/rest/api/Entity.md#pk) and [Entity.process()](https://dataclient.io/rest/api/Entity.md#process)
receive `[]`) and no updater function. Rows that share a pk merge in list order, without
[Entity.shouldReorder()](https://dataclient.io/rest/api/Entity.md#shouldreorder). Use this instead of calling `set()` once per row, such as when
[batching high-frequency stream updates](https://dataclient.io/vue/concepts/managers.md#batching).

### setResponse(endpoint, ...args, response) {#setResponse}

Stores `response` in cache for given [Endpoint](https://dataclient.io/rest/api/Endpoint.md) and args.

Any components suspending for the given [Endpoint](https://dataclient.io/rest/api/Endpoint.md) and args will resolve.

If data already exists for the given [Endpoint](https://dataclient.io/rest/api/Endpoint.md) and args, it will be updated.

```ts
const ctrl = useController();
let websocket: WebSocket;

onMounted(() => {
  websocket = new WebSocket(url);

  websocket.onmessage = event =>
    ctrl.setResponse(
      EndpointLookup[event.endpoint],
      ...event.args,
      event.data,
    );
});

onUnmounted(() => websocket.close());
```

This shows a proof of concept in Vue; however a [Manager websockets implementation](https://dataclient.io/vue/concepts/managers.md#data-stream)
would be much more robust.

### setError(endpoint, ...args, error) {#setError}

Stores the result of [Endpoint](https://dataclient.io/rest/api/Endpoint.md) and args as the error provided.

### resolve(endpoint, { args, response, fetchedAt, error }) {#resolve}

Resolves a specific fetch, storing the `response` in cache.

This is similar to setResponse, except it triggers resolution of an inflight fetch.
This means the corresponding optimistic update will no longer be applies.

This is used in [NetworkManager](https://dataclient.io/vue/api/NetworkManager.md), and should be used when
processing fetch requests.

### subscribe(endpoint, ...args) {#subscribe}

Marks a new subscription to a given [Endpoint](https://dataclient.io/rest/api/Endpoint.md). This should increment the subscription.

[useSubscription](https://dataclient.io/vue/api/useSubscription.md) and [useLive](https://dataclient.io/vue/api/useLive.md) call this on mount.

This might be useful for custom composables to sub/unsub based on other factors.

```ts
const controller = useController();

// args can be a ref, computed or getter; this re-runs when it changes
watchEffect(onCleanup => {
  const currentArgs = toValue(args);
  controller.subscribe(endpoint, ...currentArgs);
  onCleanup(() => controller.unsubscribe(endpoint, ...currentArgs));
});
```

### unsubscribe(endpoint, ...args) {#unsubscribe}

Marks completion of subscription to a given [Endpoint](https://dataclient.io/rest/api/Endpoint.md). This should
decrement the subscription and if the count reaches 0, more updates won't be received automatically.

[useSubscription](https://dataclient.io/vue/api/useSubscription.md) and [useLive](https://dataclient.io/vue/api/useLive.md) call this on unmount.

## Data Access

### get(schema, ...args, state) {#get}

Looks up any [Queryable](https://dataclient.io/rest/api/schema.md#queryable) [Schema](https://dataclient.io/rest/api/schema.md#schema-overview) in `state`.

#### Example

This is used in [useQuery](https://dataclient.io/vue/api/useQuery.md) and can be used in
[Managers](https://dataclient.io/vue/api/Manager.md) to safely access the store.

In components, [useQuery()](https://dataclient.io/vue/api/useQuery.md) keeps the result reactive. In event handlers, pass
[getState()](#getState) to read the latest store:

```ts
const ctrl = useController();

const toggle = (id: string) => {
  const todo = ctrl.get(Todo, { id }, ctrl.getState());
  if (todo) ctrl.set(Todo, { id }, { id, completed: !todo.completed });
};
```

### getResponse(endpoint, ...args, state) {#getResponse}

```ts title="returns"
{
  data: DenormalizeNullable<E['schema']>;
  expiryStatus: ExpiryStatus;
  expiresAt: number;
}
```

Gets the (globally referentially stable) response for a given endpoint/args pair from state given.

#### data

The denormalize response data. Guarantees global referential stability for all members.

#### [expiryStatus](https://dataclient.io/vue/concepts/expiry-policy.md#expiry-status)

```ts
export enum ExpiryStatus {
  Invalid = 1,
  InvalidIfStale,
  Valid,
}
```

##### Valid

- Will never suspend.
- Might fetch if data is stale

##### InvalidIfStale

- Will suspend if data is stale.
- Might fetch if data is stale

##### Invalid

- Will always suspend
- Will always fetch

#### expiresAt

A number representing time when it expires. Compare to Date.now().

#### Example

This is used in [useCache](https://dataclient.io/vue/api/useCache.md), [useSuspense](https://dataclient.io/vue/api/useSuspense.md) and can be used in
[Managers](https://dataclient.io/vue/api/Manager.md) to lookup a response with the state provided.

In event handlers, pass [getState()](#getState) to read the latest store, as in the
[getState() example](#getState).

```tsx title="MyManager.ts"
import {
  type Manager,
  type Middleware,
  actionTypes,
} from '@data-client/vue';

export default class MyManager implements Manager {
  declare protected websocket: WebSocket;

  middleware: Middleware = controller => {
    return next => async action => {
      if (action.type === actionTypes.FETCH) {
        console.log('The existing response of the requested fetch');
        console.log(
          controller.getResponse(
            action.endpoint,
            ...action.args,
            controller.getState(),
          ).data,
        );
      }
      next(action);
    };
  };

  cleanup() {
    this.websocket.close();
  }
}
```

### getError(endpoint, ...args, state) {#getError}

Gets the error, if any, for a given endpoint. Returns undefined for no errors.

### snapshot(state, fetchedAt) {#snapshot}

Returns a [Snapshot](https://dataclient.io/vue/api/Snapshot.md).

### getState() {#getState}

Gets the internal state of Reactive Data Client that has _already been [committed](https://react.dev/learn/render-and-commit#step-3-react-commits-changes-to-the-dom)_.

> **Warning**
>
> This should only be used in event handlers or [Managers](https://dataclient.io/vue/api/Manager.md).
>
> Using getState() in a `computed()` or template won't update when the store changes. Use
> [useQuery()](https://dataclient.io/vue/api/useQuery.md) or [useCache()](https://dataclient.io/vue/api/useCache.md) there instead.

```ts
const controller = useController();

const handleShare = () => {
  // reads the latest store without making this handler reactive
  const { data: article } = controller.getResponse(
    ArticleResource.get,
    { id: props.id },
    controller.getState(),
  );
  if (article) navigator.share({ title: article.title, url: article.url });
};
```

[Mutations](#endpointsideeffect) resolve _before_ the store is updated, so read their result from
the value `fetch()` resolves with rather than `getState()`.
