# Manager

`Managers` are singletons that handle global side-effects. Kind of like [useEffect()](https://react.dev/reference/react/useEffect) 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/docs/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](https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm)
when using \<DataProvider />. This is necessary since the commit phase is asynchronously scheduled. This enables building
managers that perform work after the DOM is updated and also 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/docs/api/DataProvider.md#managers) prop of [DataProvider](https://dataclient.io/docs/api/DataProvider.md). Be
sure to hoist to _module level_ or wrap in a _useMemo()_ to ensure they are not recreated. Managers
have internal state, so it is important to not constantly recreate them.

**Web**

```tsx title="index.tsx"
import { DataProvider, getDefaultManagers } from '@data-client/react';
import { createRoot } from 'react-dom/client';
import App from './App';
import MyManager from './MyManager';

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

createRoot(document.body).render(
  <DataProvider managers={managers}>
    <App />
  </DataProvider>,
);
```

**React Native**

```tsx title="index.tsx"
import { DataProvider, getDefaultManagers } from '@data-client/react';
import { AppRegistry } from 'react-native';
import App from './App';
import MyManager from './MyManager';

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

const Root = () => (
  <DataProvider managers={managers}>
    <App />
  </DataProvider>
);
AppRegistry.registerComponent('MyApp', () => Root);
```

**NextJS**

```tsx title="app/Provider.tsx"
'use client';
import { getDefaultManagers } from '@data-client/react';
import { DataProvider } from '@data-client/react/nextjs';
import MyManager from './MyManager';

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

export default function Provider({
  children,
}: {
  children: React.ReactNode;
}) {
  return <DataProvider managers={managers}>{children}</DataProvider>;
}
```

```tsx title="app/layout.tsx"
import Provider from './Provider';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}
```

**Expo**

```tsx title="app/_layout.tsx"
import { Stack } from 'expo-router';
import { DataProvider, getDefaultManagers } from '@data-client/react';
import MyManager from './MyManager';

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

export default function RootLayout() {
  return (
    <DataProvider managers={managers}>
      <Stack>
        <Stack.Screen name="index" />
      </Stack>
    </DataProvider>
  );
}
```

## Control flow

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

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

### Dispatching Actions

[Controller](https://dataclient.io/docs/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/react';
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/docs/api/Actions.md).

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

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/docs/api/Actions.md), we can 'consume' it by not calling next.

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

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/react';
import { actionTypes } from '@data-client/react';
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/docs/api/Actions.md).

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

## Use cases

Minimal examples for common Manager use cases:

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