npm.io
1.0.8 • Published yesterday

@2bbelmiro/web-provider

Licence
MIT
Version
1.0.8
Deps
0
Size
92 kB
Vulns
0
Weekly
0

@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 unificado WebRef.

Pré-requisitos

Certifique-se de que seu ambiente cumpre os seguintes requisitos mínimos de versão:

  • Node.js: 18.x LTS ou superior.
  • Angular: @angular/core na versão 16.x ou superior.
  • RxJS: 7.x ou 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.

Keywords