# Manager

`Managers` are singletons that handle global side-effects. Kind of like [watchEffect()](https://vuejs.org/api/reactivity-core.html#watcheffect) for the central data
store.

The default managers orchestrate the complex asynchronous behavior that Data Client
provides out of the box. These can easily be configured with [getDefaultManagers()](https://dataclient.io/vue/api/getDefaultManagers.md), and
extended with your own custom `Managers`.

Managers must implement [middleware](#middleware), which hooks them into the central store's
[control flow](#control-flow). Additionally, [cleanup()](#cleanup) and [init()](#init) hook into the
store's lifecycle for setup/teardown behaviors.

```typescript
type Dispatch = (action: ActionTypes) => Promise<void>;

type Middleware = (controller: Controller) => (next: Dispatch) => Dispatch;

interface Manager {
  middleware: Middleware;
  cleanup(): void;
  init?: (state: State<any>) => void;
}
```

## Lifecycle

### middleware

`middleware` is very similar to a [redux middleware](https://redux.js.org/advanced/middleware).
The only differences is that the `next()` function returns a `Promise`.

This promise resolves when the reducer update is committed to the
[DataClientPlugin](https://dataclient.io/vue/api/DataClientPlugin.md) store. This enables building managers that perform work with the
newly computed state.

Since redux is fully synchronous, an adapter must be placed in front of Reactive Data Client style middleware to
ensure they can consume a promise. Conversely, redux middleware must be changed to pass through promises.

Middlewares will [intercept actions](#reading-and-consuming-actions) that are dispatched and then potentially [dispatch their own actions](#dispatching-actions) as well.
To read more about middlewares, see the [redux documentation](https://redux.js.org/advanced/middleware).

### init(state) {#init}

Called with initial state after provider is mounted. Can be useful to run setup at start that
relies on state actually existing.

### cleanup()

Provides any cleanup of dangling resources after manager is no longer in use.

## Adding managers to Reactive Data Client {#adding}

Use the [managers](https://dataclient.io/vue/api/DataClientPlugin.md#managers) option of [DataClientPlugin](https://dataclient.io/vue/api/DataClientPlugin.md). The plugin is
installed once per app, so managers are created once.

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

const managers = [...getDefaultManagers(), new MyManager()];

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

## Control flow

Managers integrate with the DataClientPlugin store with their lifecycles and middleware. They orchestrate complex control
flows by interfacing via intercepting and dispatching [actions](https://dataclient.io/vue/api/Actions.md), as well as reading the internal state.

The job of `middleware` is to dispatch actions, respond to [actions](https://dataclient.io/vue/api/Actions.md), or both.

### Dispatching Actions

[Controller](https://dataclient.io/vue/api/Controller.md) provides type-safe action dispatchers.

```ts title="CurrentTime"
import { Entity } from '@data-client/rest';

export default class CurrentTime extends Entity {
  id = 0;
  time = 0;
}
```

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

export default class TimeManager implements Manager {
  declare protected intervalID?: ReturnType<typeof setInterval>;

  middleware: Middleware = controller => {
    this.intervalID = setInterval(() => {
      controller.set(CurrentTime, { id: 1 }, { id: 1, time: Date.now() });
    }, 1000);

    return next => async action => next(action);
  };

  cleanup() {
    clearInterval(this.intervalID);
  }
}
```

### Reading and Consuming Actions

`actionTypes` includes all constants to distinguish between different [actions](https://dataclient.io/vue/api/Actions.md).

```ts
import type { Manager, Middleware } from '@data-client/vue';
import { actionTypes } from '@data-client/vue';

export default class LoggingManager implements Manager {
  middleware: Middleware = controller => next => async action => {
    switch (action.type) {
      case actionTypes.SET_RESPONSE:
        if (action.endpoint.sideEffect) {
          console.info(
            `${action.endpoint.name} ${JSON.stringify(action.response)}`,
          );
          // wait for state update to be committed
          await next(action);
          // get the data from the store, which may be merged with existing state
          const { data } = controller.getResponse(
            action.endpoint,
            ...action.args,
            controller.getState(),
          );
          console.info(`${action.endpoint.name} ${JSON.stringify(data)}`);
          return;
        }
      // actions must be explicitly passed to next middleware
      default:
        return next(action);
    }
  };

  cleanup() {}
}
```

In conditional blocks, the action [type narrows](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#working-with-union-types),
encouraging safe access to its members.

In case we want to 'handle' a certain [action](https://dataclient.io/vue/api/Actions.md), we can 'consume' it by not calling next.

```ts title="isEntity"
import type { Schema, EntityInterface } from '@data-client/vue';

export default function isEntity(
  schema: Schema,
): schema is EntityInterface {
  return schema !== null && (schema as any).pk !== undefined;
}
```

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

export default class CustomSubsManager implements Manager {
  declare protected entities: Record<string, EntityInterface>;

  middleware: Middleware = controller => next => async action => {
    switch (action.type) {
      case actionTypes.SUBSCRIBE:
      case actionTypes.UNSUBSCRIBE:
        const { schema } = action.endpoint;
        // only process registered entities
        if (schema && isEntity(schema) && schema.key in this.entities) {
          if (action.type === actionTypes.SUBSCRIBE) {
            this.subscribe(schema.key, action.args[0]?.product_id);
          } else {
            this.unsubscribe(schema.key, action.args[0]?.product_id);
          }

          // consume subscription if we use it
          return Promise.resolve();
        }
      default:
        return next(action);
    }
  };

  cleanup() {}

  subscribe(channel: string, product_id: string) {}
  unsubscribe(channel: string, product_id: string) {}
}
```

By `return Promise.resolve();` instead of calling `next(action)`, we prevent managers listed
after this one from seeing that [action](https://dataclient.io/vue/api/Actions.md).

Types: [`FETCH`](https://dataclient.io/vue/api/Actions.md#fetch), [`SET`](https://dataclient.io/vue/api/Actions.md#set), [`SET_RESPONSE`](https://dataclient.io/vue/api/Actions.md#set_response),
[`RESET`](https://dataclient.io/vue/api/Actions.md#reset), [`SUBSCRIBE`](https://dataclient.io/vue/api/Actions.md#subscribe), [`UNSUBSCRIBE`](https://dataclient.io/vue/api/Actions.md#unsubscribe),
[`INVALIDATE`](https://dataclient.io/vue/api/Actions.md#invalidate), [`INVALIDATEALL`](https://dataclient.io/vue/api/Actions.md#invalidateall), [`EXPIREALL`](https://dataclient.io/vue/api/Actions.md#expireall)

## Use cases

Minimal examples for common Manager use cases:

- [Logging](https://dataclient.io/vue/concepts/managers.md#middleware-logging)
- [Error reporting (monitoring)](https://dataclient.io/vue/concepts/managers.md#error-reporting)
- [Metrics (fetch timing)](https://dataclient.io/vue/concepts/managers.md#metrics)
- [Notifications (toasts)](https://dataclient.io/vue/concepts/managers.md#notifications)
- [Refresh on focus or reconnect](https://dataclient.io/vue/concepts/managers.md#refresh-on-focus)
- [Cross-tab synchronization](https://dataclient.io/vue/concepts/managers.md#cross-tab-sync)
- [Offline persistence](https://dataclient.io/vue/concepts/managers.md#persistence)
- [Data streams (websockets/SSE)](https://dataclient.io/vue/concepts/managers.md#data-stream)
- [Authentication: logout on 401](https://dataclient.io/vue/api/LogoutManager.md)
- [Periodic updates (interval/ticker)](#dispatching-actions)
- [Custom transport subscriptions](#reading-and-consuming-actions)
