TMF

@tmf/platform-core (6.10.0)

Published 2026-10-20 07:53:39 -03:00 by tarcisio

Installation

@tmf:registry=
npm install @tmf/platform-core@6.10.0
"@tmf/platform-core": "6.10.0"

About this package

@tmf/platform-core

Base transversal do Modelo A da plataforma TMF: o pacote agnóstico de produto que todo módulo @tmf/* adota para falar com o host e com o Hub. Reúne o HttpClient (fetch nativo com Bearer + X-Client-Code), o cliente do contrato do Hub (createHubContractClient / useAppContract, com suporte a ETag), o ModuleProvider que recebe os ports injetados pelo host (i18n, toast, theme, http) e expõe contract, tenant, user, permissions e featureFlags por contexto, além das permissões (can / usePermission / Gate) e dos adapters opcionais (sonner, react-i18next, design-tokens). Não contém nenhuma lógica de domínio — é só o contrato de integração.

Versão atual: 6.10.0 — linha gerada de package.json por scripts/gen-packages-table.py; não editar à mão.

Nota: o pacote é publicado como @tmf/platform-core; internamente os tipos e a documentação de conformidade ainda usam o nome histórico @tmf/module-kit (ver CONFORMANCE.md).

Instalação

O pacote é privado e servido pelo registry npm do Forgejo (organização TMF):

npm config set @tmf:registry https://git.trademarketingforce.com/api/packages/TMF/npm/
npm i @tmf/platform-core

react e react-dom (>=18) são peerDependencies obrigatórias. sonner, react-i18next e @tmf/design-tokens são peerDeps opcionais — só necessárias se você importar os adapters correspondentes.

Uso básico

1. HttpClient — createHttpClient

Cliente HTTP sobre fetch nativo (sem axios). Injeta Authorization: Bearer via getToken e X-Client-Code quando clientCode é definido (multi-tenant):

import { createHttpClient } from '@tmf/platform-core';

const http = createHttpClient({
  baseUrl: 'https://hub.trademarketingforce.com',
  getToken: () => getMemoryToken() ?? '',
  clientCode: 'ACME', // opcional — vira X-Client-Code
});

const tickets = await http.request<Ticket[]>('/api/tickets');

Opções para hosts reais (todas aditivas — request continua devolvendo só o data):

Opção Onde Efeito
credentials cliente e request Repassado ao fetch ('include' p/ cookie). A request sobrescreve o cliente.
body cru request FormData, Blob, ArrayBuffer/views e URLSearchParams vão sem JSON.stringify e sem Content-Type forçado (o fetch define o boundary). Demais valores seguem como JSON.
onUploadProgress request O fetch não tem progresso de upload. Quando informado (e XMLHttpRequest existe — só navegador), a request é feita via XHR, com os mesmos headers/credentials/signal/timeout; sem ele, usa fetch. Recebe { loaded, total, progress? }.
responseType request 'json' (padrão) | 'text' | 'blob' | 'arraybuffer'.
params request Objeto serializado na query: arrays repetem a chave, undefined/null são omitidos, mescla com ?x= já presente no path.
timeout (ms) cliente e request Aborta via AbortController, combinado com o signal do chamador. Estouro lança HttpTimeoutError (code: 'ETIMEDOUT'); abort do chamador segue sendo AbortError.
requestFull cliente Como request, mas devolve { data, status, headers } (headers com nomes em minúsculas).
retry cliente e request Retry de 429/5xx (padrão: GET/HEAD, 2 tentativas adicionais, backoff 200ms). false desliga (1 chamada); true = padrão; no cliente aceita também { maxRetries, baseDelayMs, methods }; na request, { attempts }. A request sobrescreve o cliente.
onUnauthorized cliente () => Promise<void | boolean>. Em 401, renova a credencial (getToken passa a devolver o token novo) uma vez para N requests simultâneas; cada uma se repete uma vez. false/rejeição = falha: rejeita com o HttpError 401 original. A repetição com 401 não dispara novo refresh.
onAuthFailure cliente (error: HttpError) => void, chamado quando o refresh falha — ponto para deslogar.

HttpError expõe error.response = { status, data, headers } (compatível com quem lê error.response do axios), além de status, body e headers.

const { data, headers } = await http.requestFull<Row[]>('/api/rows', { params: { ids: [1, 2], q: 'x' } });
const pdf = await http.request<Blob>('/api/report', { responseType: 'blob', timeout: 30_000 });
await http.request('/api/upload', { method: 'POST', body: formData, onUploadProgress: (e) => setPct(e.progress ?? 0) });

2. Contrato do Hub — createHubContractClient / useAppContract

Busca o AppContract em GET /api/platform/app-contract, normalizando o wire snake_case do Hub para o AppContract canônico (camelCase). Suporta cache condicional via ETag (If-None-Match → 304):

import { useAppContract } from '@tmf/platform-core';

const { contract, loading, error, refresh } = useAppContract({
  baseUrl: 'https://hub.trademarketingforce.com',
  getToken: () => getMemoryToken() ?? '',
});

O hook faz refresh automático por ttlSeconds (fallback de 60s). Para uso imperativo, use createHubContractClient({ baseUrl, getToken }).fetch(etag).

error não é o sinal de tela de erro — contract === null é. Um refresh em background que falha (aba inativa, token expirado, rede) mantém o contrato anterior e preenche error. O contrato continua válido. Só a 1ª carga sem sucesso deixa contract nulo, e nesse caso o hook já retenta com backoff (5s → 15s → 60s):

const { contract, loading, error } = useAppContract({ baseUrl, getToken });
if (loading) return <Spinner />;
if (!contract) return <Indisponivel />;        // ✅ fail-closed só sem contrato
if (error) console.warn('[contract] refresh falhou, usando cache:', error.message);
return <ModuleProvider contract={contract}>…</ModuleProvider>;

// ❌ if (error || !contract) — derruba o módulo quando um refresh falha (TH-64619)

3. Provider — ModuleProvider

Monta o root do módulo com o contrato e os ports injetados pelo host. Aplica theme.cssVars no :root, carrega locale.messages no i18n e cria o http automaticamente se não for passado:

import { ModuleProvider } from '@tmf/platform-core';
import { createSonnerToastPort } from '@tmf/platform-core/adapters/sonner';

<ModuleProvider
  contract={contract}
  moduleKey="meu-modulo"   // seleciona services[key='meu-modulo'] para o http default
  toast={createSonnerToastPort()}
  getToken={() => getMemoryToken() ?? ''}
>
  <MeuModulo />
</ModuleProvider>

moduleKey resolve a baseUrl do http pelo service do próprio módulo em contract.services[] (key, ou alias de wire module) — sem entrada, cai em '' (same-origin) com aviso, nunca no service de outro módulo. Omitido, um contrato com um único service usa esse service; com vários, services[0] (legado). [TH-63406]

Dentro da árvore, consuma via hooks: useModule(), useTenant(), useCurrentUser(), useModuleHttp(), useFeatureFlag(key), useServiceEndpoint(key). Guarde permissões com usePermission('perm') ou <Gate require="perm">…</Gate>.

Posição no Modelo A

@tmf/platform-core é a base de todos os módulos da plataforma. No Modelo A, a configuração (tenant, usuário, permissões, tema, locale, feature flags, endpoints de serviço) é servida pelo Hub via app-contract e entregue ao módulo por contexto — o módulo nunca lê localStorage direto nem importa sonner/react-i18next/design-tokens diretamente, apenas via ports/adapters do core.

Isso mantém o pacote agnóstico de produto: ele define o contrato de integração (ports, contrato Hub, permissões, provider) e nenhum comportamento de negócio. Cada módulo @tmf/* declara @tmf/platform-core como peerDependency e cumpre o checklist de CONFORMANCE.md.

Dependencies

Development dependencies

ID Version
@types/react ^18.2.47
@types/react-dom ^18.2.18
jsdom ^25.0.1
sonner ^1.7.4
typescript ^5.3.3

Peer dependencies

ID Version
react >=18.0.0
react-dom >=18.0.0

Keywords

tmf module-kit platform contract
Details
npm
2026-10-20 07:53:39 -03:00
1381
Trade Marketing Force LTDA
UNLICENSED
62 KiB
Assets (1)
Versions (28) View all
6.13.0-rc.1 2026-11-04
6.12.0-rc.2 2026-11-04
6.12.0-rc.1 2026-11-03
6.11.0 2026-10-29
6.10.0 2026-10-20