@brickflow/http
v0.0.22
Published
Minimal HTTP transport and Nuxt state helpers for brickflow.
Readme
@brickflow/http
Минимальный HTTP-пакет для brickflow:
createHttpдля низкоуровневыхGET/POSTcreateUseHttpдля Nuxt state-обёртки с cache, SSR и ручнымfetchdefineGetдля типизированных endpoint-фабрикcreateUseCaseдля DI-паттерна в доменных модулях
Актуальные примеры в репозитории лежат в apps/playground.
Exports
Рекомендуемый импорт:
import { createHttp, createUseHttp, createUseCase, defineGet } from '@brickflow/http'Из пакета также экспортируются:
createURLhashData- типы
HttpClient,HttpConfig,HttpEndpoint,HttpKey,HttpParam,HttpResponse,HttpResponseData - Nuxt-типы
UseHttpFn,UseHttpOptions,UseHttpResult
Сабпуть @brickflow/http/nuxt остаётся доступным, но в текущей кодовой базе playground использует импорты из корня.
Типизация API
Пакет не знает схему вашего API заранее. Её нужно объявить в проекте через declare module '@brickflow/http'.
Актуальный пример из apps/playground/types/http.d.ts:
import type { HttpKey, HttpResponseData } from '@brickflow/http'
type Convention = {
'/api/http-error-demo': null
'/products':
| {
limit: number
products: {
category: string
id: number
price: number
rating: number
thumbnail: string
title: string
}[]
skip: number
total: number
}
| {
message: 'error'
}
}
declare module '@brickflow/http' {
interface HttpConfig<TKey extends HttpKey = HttpKey> {
ignore?: (data: HttpResponseData<TKey>) => boolean
}
interface HttpEndpoint extends Convention {}
}
export {}Что это даёт:
HttpKeyстановится union из endpoint-ключейhttpClient.get('/products')получает корректный тип ответаcreateGet<Params>()('/products')наследует тот же контрактHttpConfigможно расширять своими полями, как в playground черезignore
Если нужен типизированный error в UseHttpResult, дополнительно расширьте HttpErrorMap. По умолчанию error имеет тип never, а разделение между data и error зависит только от runtime isError.
createHttp
Актуальный пример из apps/playground/plugins/01.di.ts:
import { createHttp } from '@brickflow/http'
import { handlePlaygroundResponse } from '~/core/http-error'
const httpClient = createHttp({
baseURL: 'https://dummyjson.com',
headers: {
'X-Playground-Http': 'playground-runtime',
},
onResponse: handlePlaygroundResponse,
})Поддерживаемые опции:
baseURL: stringarrayMode?: 'json' | 'repeat'fetch?: typeof fetchheaders?: Record<string, string | undefined> | (() => Record<string, string | undefined>)onResponse?: (response) => void | Promise<void>timeout?: number
GET
const response = await httpClient.get('/products', {
params: {
limit: 10,
},
retry: {
delay: 300,
retries: 3,
},
})Особенности:
- query строится из
params GETподдерживает retry для429и5xx- delay экспоненциальный:
delay * 2 ** attempt signalможно передать как одинAbortSignalили массивAbortSignal[]
POST
const { data } = await httpClient.post('/api/http-error-demo')Для POST:
Record<string, unknown>сериализуется в JSONFormDataотправляется как есть- retry нет
- если response не JSON, пакет вернёт пустой объект в
data
Расширение HttpConfig
Так как HttpConfig расширяемый, можно прокидывать свои поля в запрос и читать их в onResponse.
Пример из playground:
httpClient.get('/products', {
ignore: (data) => data?.limit === 20,
})export function handlePlaygroundResponse(
response: Parameters<NonNullable<CreateHttpOptions['onResponse']>>[0],
): void {
console.log(response.config.ignore?.(response.data))
}createUseHttp
В apps/playground/core/http.ts app-level инстанс создаётся один раз:
import { createUseHttp, defineGet } from '@brickflow/http'
import { dbDeleteKeysWithPart, dbGet, dbSafeSet } from './indexdb'
const CACHE_DB_NAME = 'smart-cache-v2'
const CACHE_STORE_NAME = 'playground-http'
const useHttp = createUseHttp({
getCache: () => ({
deleteKeysWithPart: async (part: string) => await dbDeleteKeysWithPart(part, CACHE_DB_NAME, CACHE_STORE_NAME),
get: async <T>(key: string) => await dbGet<T>(key, CACHE_DB_NAME, CACHE_STORE_NAME),
set: async <T>(key: string, value: T, ttl: number) =>
await dbSafeSet<T>(key, value, CACHE_DB_NAME, CACHE_STORE_NAME, ttl),
}),
getHttpClient: () => useNuxtApp().$http,
isDev: () => false,
})
export const createGet = defineGet(useHttp)Зависимости:
getHttpClientобязательноgetCacheопциональноisDevопциональноisErrorопциональноttlопциональноchannelNameопционально
createUseHttp возвращает функцию useHttp(options), которая создаёт реактивное состояние запроса:
dataerrorpendingpendingCachehasFirstDatahasFreshDatafetch(params?, { signal? })
Опции useHttp
urlinitParamsmapParamseffectisErrorlazyserver
effect(data, config) вызывается и для cache, и для fresh-response:
config.cached === trueдля cacheconfig.cached === falseдля сетиconfig.paramsсодержит уже применённые params
defineGet
defineGet связывает app-level useHttp с endpoint-фабриками:
const useHttp = createUseHttp({ ... })
export const createGet = defineGet(useHttp)Дальше в домене можно описывать запросы коротко и типизированно:
products: createGet<{
limit: number
}>()('/products')Path-параметры можно передавать тем же объектом params:
productOne: createGet<{
productId: number
}>()('/products/:productId')При вызове productId будет подставлен в URL, а не уйдёт в query string:
const productHttp = await app.$di.product.productOne({
lazy: true,
})
await productHttp.fetch({
productId: 3,
})Если generic не передан, :segment автоматически типизируется как string:
productOne: createGet()('/products/:productId')Если generic передан, path params мерджатся с ним:
productOne: createGet<{
test: number
}>()('/products/:productId')В этом случае тип params будет таким:
{
productId: string
test: number
}Path params нужно передавать явно:
const productHttp = await app.$di.product.productOne({
lazy: true,
})
await productHttp.fetch({
productId: '3',
})Если нужно явно переопределить тип ответа для конкретного endpoint-а, можно добавить ещё один generic-слой:
products: createGet<{
limit: number
}>()<{
test: string
}>('/products')defineGet поддерживает два слоя настроек:
- Базовые настройки endpoint-а при объявлении:
const products = createGet<{ limit: number }>()('/products', {
mapParams: (params) => ({
limit: params?.limit ?? 10,
}),
})- Runtime-опции при вызове:
const productsHttp = await products({
initParams: { limit: 20 },
lazy: true,
server: false,
})Если effect, mapParams или isError указаны и при объявлении, и при вызове, пакет объединяет их так:
effectвызывает оба обработчикаisErrorиз runtime имеет приоритетmapParamsиз runtime имеет приоритет
createUseCase
В playground доменный модуль собирается через createUseCase:
import { createUseCase } from '@brickflow/http'
import { createGet } from '~/core/http'
export default createUseCase()(({ httpClient }) => {
return {
async apiError() {
const { data } = await httpClient.post('/api/http-error-demo')
return data
},
products: createGet<{
limit: number
}>()('/products'),
}
})Это даёт простой контракт для DI: use-case получает httpClient, а внутри может смешивать:
- прямые mutation/one-shot запросы через
httpClient - stateful GET-запросы через
createGet
Использование в компоненте
Актуальный пример из apps/playground/pages/index.vue:
<script lang="ts" setup>
const app = useNuxtApp()
const httpProducts = await app.$di.product.products({
lazy: true,
})
</script>
<template>
<button
type="button"
@click="httpProducts.fetch()"
>
{{ httpProducts.hasFirstData }} Action
</button>
{{ httpProducts.data }}
</template>Типичный сценарий:
- Создать request-state через
await endpoint({ ...options }) - Если
lazy: true, вызватьfetch()вручную - Читать
data,error,pending,pendingCache
Пример с параметрами:
const productsHttp = await app.$di.product.products({
initParams: {
limit: 10,
},
lazy: true,
})
await productsHttp.fetch({
limit: 30,
})Cache и синхронизация между вкладками
Если передан getCache, createUseHttp:
- сначала пробует отдать cache
- вызывает
effect(..., { cached: true })для cache-ответа - затем делает сетевой запрос
- сохраняет успешный ответ в cache
- при изменении хеша удаляет связанные cache-ключи через
deleteKeysWithPart
Дополнительно пакет использует BroadcastChannel и синхронизирует свежие успешные GET-ответы между вкладками.
По умолчанию:
ttl = 7 днейchannelName = 'http-tab-sync'
SSR
Если вызвать endpoint с server: true, на сервере пакет использует useLazyAsyncData, сохраняет результат в useState и переиспользует его на клиенте после гидрации.
Это полезно для страниц, где нужен первый ответ уже в SSR, но тот же контракт data / pending / fetch должен остаться и на клиенте.
Вспомогательные функции
import { createURL, hashData } from '@brickflow/http'createURL(url, params)строит query stringhashData(data)считает SHA-1 черезcrypto.subtle, а без него использует fallback-хеш
Когда что использовать
createHttp:
- POST/мутации
- one-shot запросы
- когда не нужен reactive state
createUseHttp + defineGet:
- SSR-friendly GET
- cache и
pendingCache - повторное использование endpoint-описаний
- ручной
fetch()
createUseCase:
- доменные модули с DI
- единый контракт для доступа к
httpClient
