@elcrm/api
v0.1.55
Published
HTTP-клиент для elCRM: JSON/FormData/crypto/binary, таймауты, отмена, прогресс и React-хук useQuery.
Readme
@elcrm/api
HTTP-клиент для elCRM: JSON / FormData / crypto / binary, таймауты, отмена, прогресс загрузки и React-хук useQuery.
Установка
npm install @elcrm/api
# или
bun add @elcrm/apiБыстрый старт
import { Api } from "@elcrm/api";
Api.create({
url: "https://api.example.com",
method: "POST",
timeout: 15000,
storageKey: "elcrm-token",
});
try {
// JSON: `{ status: "success", data }` → сразу data
const users = await Api.query<User[]>("users/list", { page: 1 });
} catch (e) {
// error / logout / сеть → Error
console.error(e instanceof Error ? e.message : e);
}Api.create / Api.client
Api.create — клиент по умолчанию (Api.query, …).Api.client — независимый клиент со своим url. Один storageKey — общий ssid.
| Поле | Описание |
| ------------ | ------------------------------------------------------------------------ |
| url | Базовый URL (можно не задавать при resolveUrl / urlKey) |
| resolveUrl | () => string \| null — base на каждый запрос (сессия / multi-tenant) |
| urlKey | Читать/писать tenant origin (url side-field); + urlPath |
| urlPath | Суффикс к urlKey (по умолчанию /api/) |
| port | Опционально |
| portKey | Ключ localStorage для side-field port (по умолчанию w) |
| key | Ключ для шифрования (по умолчанию для encryptQuery) |
| worker | true — Web Worker, если не мешают таймаут / сигнал / прогресс / binary |
| method | HTTP-метод по умолчанию |
| paths | Именованные пути относительно базового URL |
| timeout | Таймаут по умолчанию, мс (0 — без лимита) |
| storageKey | Ключ storage для ssid (по умолчанию d) |
| headers | Заголовки на каждый запрос (params.headers перекрывают) |
Глобально (до create): Api.configure({ storage?, onLogout? }) — см. React Native.
Path-RPC protocol (status + side-fields)
Контракт с @elcrm/server:
| status | Клиент |
| --------- | ------ |
| success | вернуть data |
| error | throw |
| logout | Api.Logout() + throw |
| refresh | сохранить ssid, один раз повторить запрос; повторный refresh → logout |
Побочные поля (соседи status/data) — пишутся в storage и снимаются до unwrap:
| Поле | Куда |
| ------ | ---- |
| ssid | storageKey |
| url | urlKey (если задан) |
| port | portKey |
// Сервер: rpcOk({ user }, { ssid, url })
// Клиент после query получает только { user }; ssid/url уже в storageJSON-ответы: success → data, error/logout/refresh(исчерпан) → throw.responseAs: "blob" | "text" | "arraybuffer" — без unwrap.
const KEY = "elwms_ssid";
// tenant: url из сессии на каждый запрос
Api.create({
method: "POST",
storageKey: KEY,
resolveUrl: () => {
const origin = localStorage.getItem("elwms_api_url");
return origin ? `${origin.replace(/\/+$/, "")}/api/` : null;
},
});
const auth = Api.client({
url: "https://auth.example.com/api/",
method: "POST",
storageKey: KEY,
headers: { "X-Elwms-App": "web" },
});
const products = await Api.query<Product[]>("products/list", {});
const session = await auth.query<LoginResult>("auth/login", { email, password });Api.query
await Api.query("users/list", { page: 1 }, { method: "POST", timeout: 10_000 });Тело (type):
json(по умолчанию) —JSON.stringify(data)formdata—FormDatacrypto— строка (см.encryptQuery)binary—Blob/ArrayBuffer/TypedArray(Content-Type: application/octet-streamпо умолчанию)
Ответ (responseAs):
| responseAs | Результат |
| ------------- | ---------------------------------------- |
| json | JSON + side-fields + status (ssid/url/port, logout/refresh) |
| text | string |
| arraybuffer | ArrayBuffer |
| blob | Blob |
Параметры запроса: method, type, responseAs, headers, timeout, signal, onUploadProgress, onDownloadProgress.
При ошибке сети / отмене / таймауте / HTTP ≠ 2xx / невалидном JSON возвращается ApiQueryFailure (status: "error", httpStatus, message, …) — без throw в типичных ветках.
Ответ { status: "logout" } (часто HTTP 401) вызывает Api.Logout().
Ответ { status: "refresh", ssid } — сохранить ssid и один раз повторить запрос.
Шифрование и FormData
await Api.encryptQuery("secure/echo", { a: 1 }, { key: "..." });
await Api.formDataQuery("upload", formData, { method: "POST" });Бинарные данные
await Api.query("upload/raw", new Uint8Array([1, 2, 3]), {
type: "binary",
method: "PUT",
});
const file = await Api.query<ArrayBuffer>(
"export/file",
{},
{
method: "GET",
responseAs: "arraybuffer",
},
);React: useQuery
const [status, data, update] = Api.useQuery("users/list", { page: 1 });status:"pending"|"error"|"success"update(body, signal?)— повторный запрос- при смене
link/bodyпредыдущий запрос отменяется
Мемоизируйте body, если это объект.
Сессия и утилиты
Api.getSSID();
Api.url(); // paths из create
Api.image("/avatar.png");
Api.Logout(); // очистка storage + reload (или onLogout)React Native / custom storage
По умолчанию: localStorage. Если его нет — in-memory (ssid до перезапуска JS).
Без window дефолтный Logout не делает location.reload.
Logout по умолчанию чистит только ключи клиентов (clearMode: "keys").
import { Api } from "@elcrm/api";
Api.configure({
storage: {
getItem: (k) => syncCache.get(k) ?? null,
setItem: (k, v) => syncCache.set(k, v),
removeItem: (k) => syncCache.delete(k),
},
onLogout: () => {
// навигация на Login, без reload
},
onSsidChange: (ssid) => {
// realtime / socket reconnect
},
clearMode: "keys", // или "all"
});
Api.create({ method: "POST", storageKey: "elwms_ssid", urlKey: "elwms_api_url", … });storage должен быть синхронным. Для AsyncStorage — in-memory кэш + гидрация до первого query.
Worker / fetch / XHR
worker: trueбезsignal, ненулевогоtimeout, прогресса и binary-контура → Web Worker- иначе основной поток:
fetchили ленивый чанкxhr-jsonпри прогрессе
Типы
TMethod, ProgressPayload, ApiQueryFailure, TParams, TDefaultParams, TEncryptParams, RequestPayloadType, ResponseBodyKind.
Разработка и публикация
Локально: dev.md.
npm / CI: deploy.md.
Лицензия
MIT
