# Ресурс (Resource)

Ресурс — абстракция для **чтения данных** с автоматическим кэшированием и stale-while-revalidate (SWR). Для операций записи используйте [команду][command].

Аналог: `useQuery` в TanStack Query, `query endpoint` в RTK Query.


## Создание ресурса

```typescript
const usersResource = api.createResource({
  queryFn: async (args: { page: number }, abortSignal) => {
    const res = await fetch(`/api/users?page=${args.page}`, { signal: abortSignal });
    return res.json();
  },
});
```

`queryFn` — единственная обязательная опция. Принимает аргументы запроса и `AbortSignal`, возвращает промис с данными. При отмене запроса (смена аргументов, размонтирование) сигнал срабатывает автоматически.

Вместо промиса `queryFn` может вернуть `Observable<TData>` — запись станет «живой» и будет обновляться с каждой эмиссией (WebSocket, SSE и т. п.). См. [стриминговые запросы][stream-query].


## Опции

Полный список опций — см. [API-справочник ресурса][api-resource].


## API ресурса

Полный список методов — см. [API-справочник ресурса][api-resource].


## React: useResource

Для работы в React подключите `reactHooksPlugin()` при создании API:

```typescript
import { createApi, reactHooksPlugin } from '@fozy-labs/rx-toolkit';

const api = createApi({
  plugins: [reactHooksPlugin()],
});
```

`useResource` — метод на экземпляре ресурса, доступный после подключения плагина:

```tsx
function UsersList({ page }: { page: number }) {
  const { data, error, isLoading } = usersResource.useResource({ page });

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}
```

Поведение хука:

1. При монтировании — запускает запрос с переданными аргументами.
2. При изменении аргументов — автоматически перезапрашивает данные.
3. При размонтировании — отписывается. Кэш-запись сохраняется в течение `retentionTime`.
4. При повторном монтировании с теми же аргументами — данные берутся из кэша мгновенно.


## Условные запросы

Передайте `SKIP` вместо аргументов, чтобы отложить запрос:

```tsx
import { SKIP } from '@fozy-labs/rx-toolkit';

function UserProfile({ userId }: { userId: string | null }) {
  const { data, isLoading } = userResource.useResource(
    userId ? { id: userId } : SKIP,
  );

  if (!userId) return <p>Выберите пользователя</p>;
  if (isLoading) return <Spinner />;
  return <h1>{data.name}</h1>;
}
```

`SKIP` полностью останавливает наблюдение — запрос не выполняется, состояние сбрасывается в `idle`.


## Состояния ресурса

`useResource` возвращает объект с полями `status`, `data`, `error` и булевыми флагами:

| Поле | Тип | Описание |
|---|---|---|
| `status` | `TAgentStatus` | `'idle'` · `'pending'` · `'success'` · `'error'` · `'refreshing'` · `'refresh-error'` |
| `data` | `TData \| null` | Данные последнего успешного ответа. Сохраняются при `refreshing`. |
| `error` | `TError \| null` | Ошибка последнего запроса. По умолчанию `unknown`; типизируется опцией API [`mapError`](../api/README.md#типизация-ошибок-maperror). |
| `isLoading` | `boolean` | `true` при любом незавершённом запросе. |
| `isInitialLoading` | `boolean` | `true` только при первой загрузке (данных ещё нет). |
| `isSuccess` | `boolean` | `true` когда данные получены. |
| `isError` | `boolean` | `true` при ошибке. |
| `isRefreshing` | `boolean` | `true` при фоновом обновлении (SWR). |
| `isSwitching` | `boolean` | `true`, если под `refreshing` грузятся новые аргументы, а `data` — от предыдущих. |
| `isRetrying` | `boolean` | `true`, если загрузка запущена через `retry()` после ошибки; `error` при этом хранит повторяемую ошибку. |
| `isRefreshError` | `boolean` | `true` при ошибке фонового обновления. |
| `args` | `TArgs \| null` | Аргументы текущего наблюдения. |
| `dataArgs` | `TArgs \| null` | Аргументы, для которых загружены `data`. Отличаются от `args` только при SWR-fallback после смены аргументов. |

Состояние — **дискриминированное объединение**: проверка `status` или любого флага сужает типы остальных полей. `isSuccess` гарантирует `data: TData` (без `| null`), `isError` — `error: TError` (без `| null`), `isRefreshError` — что устаревшие `data` сохранены. Полная таблица вариантов — в [API агента ресурса][api-res-agent].

```tsx
const state = usersResource.useResource({ page });

if (state.isError) {
  return <ErrorMessage error={state.error} />; // error: TError, не TError | null
}
if (state.isSuccess) {
  return <List items={state.data} />;          // data: TData, не TData | null
}
```

### Фоновое обновление (refresh)

Вызов `refresh(args)` или `prefetch(args, { force: true })` обновляет данные **без потери текущего отображения**. Пользователь продолжает видеть прежние данные, пока в фоне выполняется новый запрос. Когда ответ приходит — данные обновляются на месте; если запрос падает с ошибкой, прежние данные сохраняются, а статус переходит в `refresh-error`.

### Плавная смена аргументов (SWR)

Когда аргументы `useResource` меняются (например, пользователь переключает страницу), компонент **не сбрасывается в пустое состояние**. Вместо этого на экране остаются данные предыдущего запроса, пока загружаются новые. Как только новые данные готовы, они автоматически заменяют старые.


## Императивный API

### prefetch / ensure / fetch

```typescript
// Прогреть кэш, результат не нужен (fire-and-forget, никогда не реджектит)
void usersResource.prefetch({ page: 1 });

// Дождаться данных: кэш-хит отдаётся сразу, холодный запрос запускается и ждётся
const data = await usersResource.ensure({ page: 1 });

// Всегда свежие данные
const fresh = await usersResource.fetch({ page: 1 });
```

Параллельные вызовы с одинаковыми аргументами дедуплицируются — все ждут один общий in-flight запрос. Детали (отмена, retention, `force`) — в [API ресурса][api-resource].

`void` перед `prefetch` нужен только чтобы унять `@typescript-eslint/no-floating-promises`: сам промис не реджектится, обрабатывать нечего. Как разрешить вызов в конфиге линтера и писать без `void` — в [API ресурса][prefetch-lint].

Прежний метод `trigger(args, doForce?)` объявлен **deprecated**: `trigger(args)` ≈ `prefetch(args)`, `trigger(args, true)` ≈ `prefetch(args, { force: true })`. Отличие: на записи в состоянии `error` `prefetch` в обоих режимах делает ретрай, а `trigger` её не трогал.

### refresh

```typescript
usersResource.refresh({ page: 1 });
```

Запускает фоновый перезапрос для существующей кэш-записи — немедленно и независимо от того, есть ли у неё подписчики. Отсутствующую запись **не создаёт**: на неизвестных аргументах это no-op (в отличие от `fetch`). Работает только из статусов `success` и `refresh-error`; на `pending` / `error` — предупреждение в консоль и no-op (после ошибки нужен `retry`). Из `refresh-error` доступны оба: `refresh()` — обычное обновление, `retry()` — повтор с `isRetrying` и сохранённой ошибкой (см. [состояния агента][api-res-agent]).


### getEntry

Синхронно возвращает кэш-запись для указанных аргументов, или `null` если данные ещё не запрашивались. С флагом `doInitiate = true` — создаёт запись и запускает загрузку, если её ещё нет.

```ts
// Проверить, есть ли данные в кэше
const entry = usersResource.getEntry({ page: 1 });
if (entry) {
  console.log(entry.machine$().state.data);
}
```


### getEntry$

Реактивный аналог `getEntry`. **Возвращает сигнал** `ReadonlySignal<IQueryCacheEntry | null>` — не саму запись: вызов ничего не читает и не подписывает, зависимость возникает при чтении полученного сигнала в реактивном контексте (`Signal.compute`, `Signal.effect` и т. д.).

```ts
const entry$ = usersResource.getEntry$({ page: 1 });
Signal.effect(() => console.log(entry$()?.machine$().state.data));
```

Если аргументы реактивны, сигнал пересоздаётся на каждом вычислении — читать его нужно сразу, иначе внешний `Computed` вернёт сигнал и не подпишется на кэш:

```ts
const dynEntry$ = Signal.compute(() => usersResource.getEntry$({ page: page$() })());
//                                                                            ^^ чтение обязательно
```

Второй аргумент `doInitiate` (по умолчанию `false`). При `false` сигнал — чистый наблюдатель: чтение не меняет кэш и отдаёт `null`, пока записи нет. При `true` **чтение** сигнала создаёт и запускает запись, если её нет, поэтому сигнал всегда отдаёт запись — пересоздавая её при чтении даже после удаления. Создание ленивое: оно происходит при первом чтении сигнала, а не в момент вызова `getEntry$`, и само это чтение имеет побочный эффект — стартует запрос и вызывает хуки `onCacheEntryAdded` / `onQueryStarted`. Не используйте `doInitiate: true` там, где чтение должно оставаться чистым (например, в рендере React).

### getState

Синхронно возвращает упрощённое состояние ресурса для аргументов: `status`, `data`, `error` и булевые флаги (`isLoading`, `isSuccess`, `isError` и т. д.).

Подходит для императивной логики вне реактивного контекста, когда нужна моментальная проверка состояния без подписки:

```ts
const state = usersResource.getState({ page: 1 });

if (state.isSuccess) {
  console.log(state.data);
}
```

### createAgent

Агент — реактивный наблюдатель ресурса.
Он отслеживает текущую и при необходимости предыдущую запись кэша,
объединяя их в плоский вычисляемый сигнал.
Агент является строительным блоком для React-хука `useResource` и не требует явного уничтожения — внутренние сигналы деактивируются при потере подписчиков.
Полная таблица методов и статусов — в [API агента ресурса][api-res-agent].

```ts
const agent = usersResource.createAgent();
agent.set({ page: 1 });
agent.start();
// agent.state$() → { status: "pending", data: null, isInitialLoading: true, ... }
```

При смене аргументов через `set(newArgs)` агент реализует SWR-поведение:
    если предыдущая запись **уже содержит данные** (статус `success`, `refreshing` или `refresh-error`),
    они сохраняются в `data`, а `status` переключается на `"refreshing"` до получения нового ответа.
Это позволяет показывать устаревшие данные вместо пустого состояния.
Если предыдущий запрос ещё не завершился (`pending`), переносить нечего — агент уйдёт в `pending` с `data: null`.

```ts
// page:1 уже загрузилась (success)
agent.set({ page: 2 }); // SWR: data от page:1, status: "refreshing"
agent.set(SKIP);        // idle: data: null, status: "idle"
```


## Связи (Links)

Связи позволяют декларативно связать команду с ресурсами — подробнее в [руководстве по связям][links].


## Хуки жизненного цикла

Хуки позволяют реагировать на события кэша — подробнее в [руководстве по жизненному циклу][lifecycle].


## См. также

- [Команда][command] — мутации (создание, обновление, удаление)
- [Стриминговые запросы][stream-query] — `Observable` в queryFn: живые данные
- [Машина состояний запроса][machine] — детали переходов между статусами
- [Кэш][cache] — система кэширования записей
- [Агент][agent] — SWR-наблюдатель, связывающий UI с записью кэша
- [Кросс-табовая синхронизация][broadcast] — синхронизация кэша между вкладками

[command]: ./command.md
[stream-query]: ./stream-query.md
[machine]: ../concepts/machine.md
[api-resource]: ../api/resource.md
[prefetch-lint]: ../api/resource.md#prefetch-и-no-floating-promises
[lifecycle]: ./lifecycle.md
[links]: ./links.md
[cache]: ../concepts/cache.md
[agent]: ../concepts/agent.md
[api-res-agent]: ../api/resource-agent.md
[broadcast]: ./broadcast.md
