@2bbelmiro/web-provider
Um provider robusto, reativo e extensível para gerenciamento, compartilhamento e persistência de estado em aplicações Angular de alta performance. Desenvolvido com suporte a Server-Side Rendering (SSR) e estratégias de Hydration.
O web-provider permite encapsular regras de negócio em singletons inteligentes, provendo estados reativos de forma transparente.
Principais Recursos
- Dupla Reatividade Simétrica: Gerenciamento de estado reativo baseado em RxJS (
WebNotifier) e em Angular Signals (WebSignalNotifier). - Segurança contra Memory Leaks no SSR: Isolamento de estado nativo por aplicação através de stores em memória autocontidas, eliminando vazamentos de estado cruzado no servidor.
- Ciclos de Vida Avançados: Ganchos internos (
onInit,onAccess) e interceptadores de dados (shouldUpdateState,onBeforeSave,onBeforeLoad) que garantem controle total sobre o fluxo do dado. - Persistência Inteligente & Desacoplada: Mecanismo interno de persistência via composição (não por herança direta de storage), suportando criptografia, ofuscação ou validações em tempo de gravação/leitura.
- Suporte Completo a Zoneless: Permite a eliminação do Zone.js em projetos modernos usando a reatividade nativa dos Signals.
- Injeção de Dependências Fluida: Acesso nativo ao sistema de DI do Angular através de injeção clássica (
inject) ou usando o parâmetro unificadoWebRef.
Pré-requisitos
Certifique-se de que seu ambiente cumpre os seguintes requisitos mínimos de versão:
- Node.js:
18.xLTS ou superior. - Angular:
@angular/corena versão16.xou superior. - RxJS:
7.xou superior.
Instalação
Adicione o pacote ao seu projeto usando seu gerenciador de pacotes:
npm install @2bbelmiro/web-provider
Dependências de Par (Peer Dependencies)
Caso ainda não as possua instaladas em seu projeto, certifique-se de ter os pacotes principais do Angular:
npm install @angular/core rxjs
Se o seu provider precisar interagir com serviços HTTP, o pacote de serviços comuns é requerido:
npm install @angular/common
Configuração Global
Para instanciar as stores de forma isolada por aplicação, evitar colisões em cenários de renderização no servidor (SSR) e configurar as políticas de persistência padrão, registre o inicializador no arquivo de configuração principal da sua aplicação Angular.
Inicialização Padrão (app.config.ts)
import { ApplicationConfig } from "@angular/core";
import { provideWebProviderInitializer } from "@2bbelmiro/web-provider";
export const appConfig: ApplicationConfig = {
providers: [provideWebProviderInitializer()],
};
Inicialização Customizada (app.config.ts)
import { ApplicationConfig } from "@angular/core";
import { provideWebProviderInitializer } from "@2bbelmiro/web-provider";
export const appConfig: ApplicationConfig = {
providers: [
provideWebProviderInitializer({
storage: {
type: "localStorage", // Opções: 'localStorage' | 'sessionStorage' | 'memory'
prefix: "my_app_prefix",
},
clearStorageOnExpire: true,
ttl: 300000, // Tempo de vida padrão: 5 minutos (em milissegundos)
zoneChangeDetection: false, // Altere para true se precisar forçar detecção em apps tradicionais com Zone.js
}),
],
};
Parâmetros de Configuração Global
| Propriedade | Tipo | Valor Padrão | Descrição |
|---|---|---|---|
storage.type |
'localStorage' | 'sessionStorage' | 'memory' |
'localStorage' |
O canal físico de persistência dos dados no navegador ou na memória RAM. |
storage.prefix |
string |
'' |
Prefixo opcional adicionado às chaves no storage para evitar colisões com outros apps. |
clearStorageOnExpire |
boolean |
false |
Se true, apaga os dados do disco de forma definitiva quando o tempo de expiração expira. |
ttl |
number |
undefined |
Tempo de expiração global (Time-To-Live) em milissegundos para as instâncias do provider. |
zoneChangeDetection |
boolean |
false |
Quando ativo, aciona a execução do Zone.js para garantir ciclos de renderização com RxJS. |
Estrutura de Pastas Sugerida
Recomenda-se organizar os seus providers em diretórios dedicados a domínios de dados ou funcionalidades para manter a manutenibilidade do código:
src/
├── app/
│ ├── app.config.ts
│ ├── app.component.ts
│ └── core/
│ └── providers/
│ ├── auth/
│ │ ├── auth.interface.ts
│ │ ├── auth.provider.ts
│ │ └── auth.provider.spec.ts
│ └── counter/
│ ├── counter.interface.ts
│ └── counter.provider.ts
Conceitos Fundamentais
webProvider
├─ Singleton Sob Demanda (Lazy Loading) -> Instanciado apenas no primeiro acesso ou leitura.
├─ Persistência Transparente ---------> Gerenciado automaticamente pela biblioteca.
├─ Reatividade RxJS (WebNotifier) -------> Fluxos assíncronos e integrados a operadores clássicos.
├─ Reatividade Signals (WebSignalNotifier)-> Ideal para arquiteturas modernas Zoneless.
└─ Isolamento por Aplicação -----------> Totalmente portável e seguro para uso com Angular SSR.
Como Utilizar
1. Usando Estado Reativo com RxJS (WebNotifier)
A classe WebNotifier é ideal quando sua lógica de negócios necessita de manipulação complexa de fluxos de eventos assíncronos utilizando operadores do RxJS.
Criação do Provider (user.provider.ts)
import { webProvider, WebNotifier, WebRef } from "@2bbelmiro/web-provider";
export interface UserState {
name: string;
logged: boolean;
role: "guest" | "user" | "admin";
}
export class UserProvider extends WebNotifier<UserState> {
constructor() {
super({
name: "",
logged: false,
role: "guest",
});
}
login(name: string, role: "user" | "admin") {
this.setState({
name: name,
logged: true,
role: role,
});
}
logout() {
this.setState({
name: "",
logged: false,
role: "guest",
});
}
}
export const userProvider = webProvider(
"user_session",
(_ref: WebRef) => new UserProvider(),
);
Consumo no Componente (user.component.ts)
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { userProvider } from "./user.provider";
@Component({
selector: "app-user",
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="name$ | async as name; else anonymous">
<p>Olá, {{ name }} (Perfil: {{ userSnapshot.role }})</p>
<button (click)="onLogout()">Encerrar Sessão</button>
</div>
<ng-template #anonymous>
<p>Nenhum usuário autenticado no momento.</p>
<button (click)="onLogin()">Entrar como Administrador</button>
</ng-template>
`,
})
export class UserComponent {
get userSnapshot() {
return userProvider.snapshot;
}
name$ = userProvider.select((state) => state.name);
onLogin() {
userProvider.login("Carlos Belmiro", "admin");
}
onLogout() {
userProvider.logout();
}
}
2. Usando Estado Reativo com Angular Signals (WebSignalNotifier)
Essa classe é a escolha ideal para aplicações desenvolvidas com foco no Angular moderno e estratégias Zoneless.
Criação do Provider (counter.provider.ts)
import { webProvider, WebSignalNotifier } from "@2bbelmiro/web-provider";
export interface CounterState {
count: number;
lastUpdated: Date | null;
}
export class CounterProvider extends WebSignalNotifier<CounterState> {
constructor() {
super({
count: 0,
lastUpdated: null,
});
}
increment() {
this.setState((state) => ({
count: state.count + 1,
lastUpdated: new Date(),
}));
}
reset() {
this.setState({
count: 0,
lastUpdated: null,
});
}
}
export const counterProvider = webProvider(
"counter_state",
() => new CounterProvider(),
);
Consumo no Componente (counter.component.ts)
import { Component } from "@angular/core";
import { counterProvider } from "./counter.provider";
@Component({
selector: "app-counter",
standalone: true,
template: `
<div>
<h3>Contador: {{ count() }}</h3>
<p>
Última atualização:
{{ lastUpdated() ? (lastUpdated() | date: "mediumTime") : "Nunca" }}
</p>
<button (click)="onIncrement()">Incrementar</button>
<button (click)="onReset()">Reiniciar</button>
</div>
`,
})
export class CounterComponent {
count = counterProvider.select((state) => state.count);
lastUpdated = counterProvider.select((state) => state.lastUpdated);
onIncrement() {
counterProvider.increment();
}
onReset() {
counterProvider.reset();
}
}
Ciclos de Vida e Interceptadores
Você pode estender o comportamento padrão dos seus providers interceptando eventos ou customizando leituras e gravações. Basta sobrescrever os seguintes métodos em sua classe especializada:
import { WebSignalNotifier } from "@2bbelmiro/web-provider";
export interface PolicyState {
policyId: string;
premiumValue: number;
sensitiveToken: string;
}
export class PolicyProvider extends WebSignalNotifier<PolicyState> {
constructor() {
super({
policyId: "N/A",
premiumValue: 0,
sensitiveToken: "",
});
}
/**
* 1. onInit(): Executado imediatamente após a restauração inicial do cache do storage.
*/
protected override onInit(): void {
console.log("Provider inicializado e estado restaurado a partir do cache.");
}
/**
* 2. onAccess(): Executado toda vez que um componente ou serviço lê ou interage com o provider.
* @param isNewInstance Indica se o provider acabou de ser alocado na memória ou se já existia na WebStore.
*/
protected override onAccess(isNewInstance: boolean): void {
if (isNewInstance) {
console.log(
"Esta é a primeira instância carregada do PolicyProvider (Lazy Loaded).",
);
}
}
/**
* 3. shouldUpdateState(): Interceptador de mudança de estado em memória.
* Retorne "false" para abortar a alteração do estado global (rejeição de mutação).
*/
protected override shouldUpdateState(
current: PolicyState,
next: PolicyState,
): boolean {
if (next.premiumValue < 0) {
console.warn(
"Operação bloqueada: O valor do prêmio de seguro não pode ser negativo.",
);
return false;
}
return true;
}
/**
* 4. onBeforeSave(): Interceptador disparado imediatamente antes de salvar no disco/storage.
* Permite alterar o objeto final (ex: criptografia) ou retornar "false" para manter o dado apenas em RAM.
*/
protected override onBeforeSave(state: PolicyState): PolicyState | boolean {
if (state.policyId === "N/A") {
return false;
}
return {
...state,
sensitiveToken: btoa(state.sensitiveToken),
};
}
/**
* 5. onBeforeLoad(): Interceptador disparado no momento da leitura/restauração do storage.
* Permite decodificar os dados físicos ou rejeitar uma persistência corrompida retornando "false".
*/
protected override onBeforeLoad(cached: PolicyState): PolicyState | boolean {
try {
return {
...cached,
sensitiveToken: atob(cached.sensitiveToken),
};
} catch (e) {
console.error("Falha ao restaurar cache do storage: Dado corrompido.");
return false;
}
}
}
Injeção de Serviços do Angular
Você pode injetar serviços Angular nativos no escopo de sua classe através de duas abordagens limpas e em conformidade com as boas práticas do Angular.
Opção A: Injeção Direta com inject() (Recomendado)
import { inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { WebNotifier, webProvider } from "@2bbelmiro/web-provider";
export interface AccountState {
users: string[];
}
export class AccountProvider extends WebNotifier<AccountState> {
private readonly http = inject(HttpClient);
constructor() {
super({ users: [] });
}
loadUsersFromApi() {
this.http.get<string[]>("/api/users").subscribe((users) => {
this.setState({ users });
});
}
}
export const accountProvider = webProvider(
"account_service",
() => new AccountProvider(),
);
Opção B: Injeção por Fábrica com WebRef
import { webProvider, WebRef, WebNotifier } from "@2bbelmiro/web-provider";
import { MyCustomLoggerService } from "./my-custom-logger.service";
export interface LoggerState {
logs: string[];
}
export class LoggerProvider extends WebNotifier<LoggerState> {
constructor(private readonly loggerService: MyCustomLoggerService) {
super({ logs: [] });
}
addLog(message: string) {
this.loggerService.logToExternalServer(message);
this.setState((state) => ({ logs: [...state.logs, message] }));
}
}
export const loggerProvider = webProvider(
"application_logger",
(ref: WebRef) => {
const loggerService = ref.inject(MyCustomLoggerService);
return new LoggerProvider(loggerService);
},
);
Comunicação Entre Providers
A comunicação entre múltiplos providers ocorre de maneira nativa e síncrona devido à natureza de singletons autogerenciados da biblioteca:
import { WebNotifier, webProvider } from "@2bbelmiro/web-provider";
import { userProvider } from "./user.provider";
export interface AuditState {
authorizedActionsCount: number;
}
export class AuditProvider extends WebNotifier<AuditState> {
constructor() {
super({ authorizedActionsCount: 0 });
}
performSecureAction() {
const userState = userProvider.snapshot;
if (userState.logged && userState.role === "admin") {
this.setState((state) => ({
authorizedActionsCount: state.authorizedActionsCount + 1,
}));
console.log("Ação autorizada com sucesso.");
} else {
console.error("Ação bloqueada: Privilégios insuficientes.");
}
}
}
export const auditProvider = webProvider(
"audit_monitor",
() => new AuditProvider(),
);
Persistência e Estratégia de Storage por Composição
A biblioteca disponibiliza a utilidade WebLocalStorage para operações isoladas de persistência que adotam os prefixos e configurações estabelecidas no inicializador global. O acesso a essa estrutura é feito por meio de composição, garantindo desacoplamento e facilitando a escrita de testes unitários.
Utilizando WebLocalStorage através de Composição
import { webProvider, WebRef, WebLocalStorage } from "@2bbelmiro/web-provider";
export interface DraftState {
content: string;
}
export class DraftProvider {
constructor(private readonly storage: WebLocalStorage) {}
saveDraft(value: string) {
this.storage.setItem("user_draft_key", value);
}
getDraft(): string {
return this.storage.getItem<string>("user_draft_key") || "";
}
clearDraft() {
this.storage.removeItem("user_draft_key");
}
}
export const draftProvider = webProvider("draft_manager", (ref: WebRef) => {
const storageService = ref.inject(WebLocalStorage);
return new DraftProvider(storageService);
});
Qual Tipo de Storage Utilizar?
| Estratégia de Storage | Armazenamento | Ciclo de Vida do Dado |
|---|---|---|
localStorage |
Persistido no Navegador | Vitalício até que seja excluído manualmente por limpeza ou comandos do código. |
sessionStorage |
Persistido no Navegador | Excluído de forma automática no fechamento da aba ou janela do browser. |
memory |
Armazenamento em RAM | Mantido apenas enquanto a página atual não for totalmente recarregada. |
Configuração de TTL (Time-To-Live)
Você pode aplicar tempos limites de retenção de dados especificamente por instâncias de providers individuais:
import { webProvider, WebSignalNotifier } from "@2bbelmiro/web-provider";
export interface TempSessionState {
oneTimeToken: string;
}
export class TempSessionProvider extends WebSignalNotifier<TempSessionState> {
constructor() {
super({ oneTimeToken: "" });
}
}
export const tempSessionProvider = webProvider(
"temporary_session",
() => new TempSessionProvider(),
{
ttl: 60000,
clearStorageOnExpire: true,
},
);
Referência da API Pública
| Assinatura | Tipo | Descrição |
|---|---|---|
webProvider(key, factory, options?) |
Function |
Registra e recupera de forma preguiçosa um provider singleton a partir da chave única fornecida. |
provideWebProviderInitializer(config?) |
Function |
Função de bootstrap obrigatória para configurar o ciclo de vida e evitar colisões em SSR. |
WebNotifier<T> |
Class |
Classe abstrata base para estados orientados a RxJS Observables. |
WebSignalNotifier<T> |
Class |
Classe abstrata base para estados orientados ao novo sistema de Angular Signals. |
WebRef |
Interface |
Objeto de referência enviado nas fábricas para resolução pontual de dependências Angular. |
WebLocalStorage |
Class |
Classe utilitária que unifica o acesso ao storage local utilizando o prefixo global do app. |
destroyProvider(key) |
Function |
Desaloca da memória RAM o provider referenciado e força sua reinicialização no próximo acesso. |
Scripts do Projeto
Os seguintes comandos estão disponíveis para execução de rotinas no ecossistema de desenvolvimento da biblioteca:
| Comando | Função |
|---|---|
npm run build |
Transpila o código-fonte TypeScript da pasta src gerando a distribuição final otimizada na pasta dist. |
npm test |
Inicia o executor de testes automatizados e suítes unitárias utilizando o motor do Vitest. |
npm run lint |
Executa a varredura estática de formatação e análise lógica de código por meio do ESLint. |
npm run test:package |
Cria o artefato local (.tgz) simulando uma distribuição limpa para validações em ambientes de sandbox. |
Licença
Distribuído sob a licença MIT. Consulte o arquivo LICENSE na raiz do repositório para obter mais detalhes.