npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ploomescrm/bridge

v1.0.0

Published

Bridge de comunicação tipado e **agnóstico de transporte** entre uma aplicação host e os apps que ela embute — um `<iframe>` no web, uma `<WebView>` no mobile.

Readme

@ploomescrm/bridge

Bridge de comunicação tipado e agnóstico de transporte entre uma aplicação host e os apps que ela embute — um <iframe> no web, uma <WebView> no mobile.

A mesma API roda no web e no mobile; só o transporte muda.

        ┌──────────────────────┐                 ┌──────────────────────────┐
        │ Host                 │   READY ◀────── │ App (iframe / webview)   │
        │ web  → iframe        │ ──────▶ INIT    │                          │
        │ mobile → WebView     │ ◀─ navigate ─▶  │  createApp(...)          │
        │  createHost(...)     │ ◀── event ───▶  │                          │
        │                      │ ◀─ request ──▶  │                          │
        └──────────────────────┘                 └──────────────────────────┘

O bridge carrega zero regra de negócio: ele só conhece o handshake, o roteamento, um canal genérico de eventos e um canal genérico de pergunta-resposta (RPC). Qualquer significado de domínio (inclusive o catálogo de rotas) vive nos apps e trafega como event / request.

Fluxos de dados

Config (Host → App) — enviado no payload INIT (HostConfig): userKey, userId, apiUrls, whitelabel, e os opcionais itemId (number) e featureFlags (Record<string, boolean>). É uma base mínima — estenda-a passando um tipo mais amplo como TConfig para createHost/createApp. apiUrls exige apenas apiUrl; adicione outros endpoints sob qualquer chave string ([endpoint: string]: string | undefined) ou modele-os no seu TConfig.

Roteamento (bidirecional) — o route adapter é uma função e não pode ser serializado, então cada lado tem o seu (onNavigate) e o outro lado o dispara com navigate(...):

  • app.navigate(to)host.onNavigate (app pede ao host para rotear).
  • host.navigate(to)app.onNavigate (host pede ao app para rotear).

to é uma string parecida com uma URL, porém sem ids (ex.: /deals/detail). Ids e query params vão em params — assim web e mobile resolvem a mesma rota com adapters diferentes. O bridge não conhece nenhuma rota: cada consumidor define sua própria união de strings e a passa no generic TRoute de createHost / createApp. O catálogo vive no código dos consumidores ou chega em runtime (por exemplo, num campo do seu TConfig estendido).

Convenção — rota desconhecida: o onNavigate do receptor deve tratar um to não reconhecido com um fallback explícito (logar e ignorar, ou abrir uma tela de erro) — o outro lado pode estar numa versão mais nova e emitir rotas que este lado ainda não conhece. Quando o emissor precisa saber antes se a rota é aceita, use o RPC (request) para perguntar.

Eventos genéricos (bidirecional) — fire-and-forget:

  • app.emit(name, data)host.onEvent({ name, data }).
  • host.emit(name, data)app.onEvent({ name, data }).

Tipe-os passando um EventMap ({ [nome]: payload }) como TEvents; o onEvent recebe uma união discriminada — switch (event.name) estreita data. Todos os payloads são restritos a JsonValue: funções, Date e refs de DOM falham no typecheck (declare o map com type, não interface).

Requisições (RPC, bidirecional) — quando um lado precisa de resposta:

  • app.request(name, params)host.onRequest(req) → promise tipada no app.
  • host.request(name, params)app.onRequest(req) → promise tipada no host.

Tipe-as com um RequestMap ({ [nome]: { params, result } }) no generic TRequests. A promise rejeita em timeout (padrão 10 s, configurável por chamada), quando o handler do outro lado lança, quando não há handler registrado, ou no dispose() — ela nunca fica pendurada. Exceções no handler chegam ao chamador só com a message do erro — não coloque dados sensíveis em mensagens de erro, elas cruzam o bridge.

Instalação

pnpm add @ploomescrm/bridge

Zero dependências de runtime. Distribui ESM + CJS + types.


Web — host

import {
  createHost,
  createIframeHostTransport,
  type HostConfig,
} from '@ploomescrm/bridge';

// O vocabulário é SEU — declare com `type` (não `interface`) e passe nos
// generics; sem eles, payloads e resultados ficam tipados como JsonValue.
type Route = '/deals/detail' | '/reports' | '/settings';
type Events = {
  'theme-changed': { dark: boolean };
  'item-updated': { id: number };
};
type Requests = {
  'can-navigate': { params: { to: string }; result: { allowed: boolean } };
  'get-state': { params: null; result: { dirty: boolean } };
};

const iframe = document.querySelector<HTMLIFrameElement>('#app-frame')!;

const host = createHost<HostConfig, Route, Events, Requests>({
  transport: createIframeHostTransport({
    iframe, // ou um getter: () => iframeRef.current
    appOrigin: APP_ORIGIN, // '*' apenas quando a origem do app não pode ser fixada
  }),
  config: () => ({
    userKey,
    userId,
    apiUrls: { apiUrl, reportsApiUrl },
    whitelabel: { enabled: true, name: 'Acme', brandColor: '#123456' },
    itemId: 42,
    featureFlags: { newEditor: true },
  }),
  // Chamado a cada READY do app (handshake inicial, reloads). É o ponto certo
  // para reagir ao app ficar disponível; `appId` diferencia múltiplos apps.
  onReady: ({ appId }) => console.log('app pronto:', appId),
  // O route adapter — decide entre roteamento nativo e externo (web).
  // Trate rotas desconhecidas com fallback explícito.
  onNavigate: ({ to, params, replace }) => {
    if (isKnownRoute(to))
      history[replace ? 'replace' : 'push'](withParams(to, params));
    else console.warn('rota desconhecida vinda do app:', to);
  },
  // Eventos genéricos vindos do app — o bridge não sabe o que significam.
  onEvent: (event) => {
    switch (event.name) {
      case 'item-updated':
        refreshItem(event.data.id);
        break;
    }
  },
  // Perguntas vindas do app — retorno (ou exceção) volta como resposta tipada.
  onRequest: (req) => {
    switch (req.name) {
      case 'can-navigate':
        return { allowed: isKnownRoute(req.data.to) };
      default:
        throw new Error(`requisição desconhecida: ${req.name}`);
    }
  },
});

// host → app: rotear o app, empurrar um evento, perguntar algo.
// Chamadas feitas antes do handshake (READY → INIT) são enfileiradas e
// entregues, em ordem, logo após ele — nunca são descartadas em silêncio.
host.navigate({ to: '/deals/detail', params: { id: 42 } });
host.emit('theme-changed', { dark: true });
const state = await host.request('get-state', null); // rejeita em 10s se não houver resposta

// depois, ex.: no unmount:
host.dispose();

Web — app embutido (conteúdo do iframe)

import {
  createApp,
  createIframeAppTransport,
  type HostConfig,
} from '@ploomescrm/bridge';

// Route / Events / Requests: as mesmas type aliases do exemplo do host.
const app = createApp<HostConfig, Route, Events, Requests>({
  transport: createIframeAppTransport({ hostOrigin: HOST_ORIGIN }),
  // Roteamento host → app: o host pediu para o app navegar internamente.
  onNavigate: ({ to, params }) => router.go(to, params),
  // Eventos host → app.
  onEvent: (event) => applyHostEvent(event),
  // Perguntas host → app.
  onRequest: (req) => answerHostRequest(req),
});

// Envia READY e resolve com o HostConfig. Rejeita (nunca pendura) em timeout,
// erro do host ou dispose — trate a falha.
const ctx = await app.connect(); // ou app.connect({ timeoutMs: 5000 })
// ctx.userKey, ctx.apiUrls, ctx.whitelabel, ctx.itemId, ctx.featureFlags

// app → host: pedir roteamento (ids/params fora da string `to`)
app.navigate({ to: '/deals/detail', params: { id: 42 } });
// app → host: emitir um evento genérico
app.emit('item-updated', { id: 42 });
// app → host: perguntar e aguardar resposta tipada
const { allowed } = await app.request('can-navigate', { to: '/reports' });

Mensagens host:navigate / host:event / host:request que chegarem antes do config inicial ficam bufferizadas e são entregues logo após o primeiro onConfig — seus handlers podem assumir que app.context existe.


Mobile — host (React Native)

O React Native não tem DOM, então o transporte host da WebView é bombeado manualmente: encaminhe as mensagens de saída para a ref da WebView e alimente de volta os dados do onMessage dela.

import { useRef, useMemo } from 'react';
import { WebView } from 'react-native-webview';
import { createHost, createWebViewHostTransport } from '@ploomescrm/bridge';

function EmbeddedApp() {
  const webViewRef = useRef<WebView>(null);

  const transport = useMemo(
    () =>
      createWebViewHostTransport({
        postToWebView: (s) => webViewRef.current?.postMessage(s),
      }),
    [],
  );

  useMemo(() => {
    createHost({
      transport,
      config: { userKey, userId, apiUrls, whitelabel, itemId: 7 },
      onNavigate: ({ to, params }) =>
        navigation.navigate(...resolveRoute(to, params)), // roteamento nativo
      onEvent: (event) => handleAppEvent(event),
    });
  }, [transport]);

  return (
    <WebView
      ref={webViewRef}
      source={{ uri: APP_URL }}
      onMessage={(e) => transport.receiveFromWebView(e.nativeEvent.data)}
    />
  );
}

Mobile — app embutido (página dentro da WebView)

O código do app é idêntico ao do app web, exceto pelo transporte:

import { createApp, createWebViewAppTransport } from '@ploomescrm/bridge';

const app = createApp({ transport: createWebViewAppTransport() });
const ctx = await app.connect();
app.navigate({ to: '/quotes/detail', params: { id: 7 } });

Dica: detecte o ambiente uma vez e escolha o transporte — tudo depois disso é compartilhado.

const transport =
  'ReactNativeWebView' in window
    ? createWebViewAppTransport()
    : createIframeAppTransport({ hostOrigin: HOST_ORIGIN });

Como funciona

  1. O app faz subscribe no seu transporte na criação (captura antecipada), e então connect() envia READY. Escutar antes de aguardar significa que um INIT que o host dispara no instante em que o frame carrega fica bufferizado, nunca é perdido.
  2. Enquanto o INIT não chega, o app reenvia o READY com backoff exponencial (250 ms dobrando até o teto de 2 s) — isso cobre o host que só assina o transporte depois do connect() do app. O reenvio para ao receber INIT, ao estourar o timeout, ou no dispose().
  3. O host responde a cada READY com INIT carregando o HostConfig (re-resolvendo a factory, se for uma). Se a factory lançar, o host chama seu onError local e envia host:error — o connect() pendente do app rejeita com esse erro.
  4. connect() resolve com essa configuração — ou rejeita no timeout (padrão 10 s, connect({ timeoutMs })), citando as causas prováveis: host ausente, origem errada (appOrigin/hostOrigin), iframe sandboxed. Ele nunca fica pendurado para sempre.
  5. onConfig dispara no config inicial e a cada atualização posterior empurrada pelo host (chame host.sendConfig() após um refresh da user key, etc.). Quando a factory é async e envios se sobrepõem, só a resolução mais recente é postada — configs nunca chegam fora de ordem.

Toda mensagem é envelopada em um Envelope carimbado com um discriminador e a versão do protocolo, de modo que mensagens de outras bibliotecas que compartilham o canal postMessage são ignoradas. No web, a checagem de origem adiciona uma segunda camada; passe '*' apenas quando a origem for genuinamente desconhecível. No app dentro de WebView, o transporte só aceita por padrão eventos com a cara da injeção nativa do react-native-webview (sem source, origem vazia) — scripts de terceiros na página não conseguem forjar um host:init via postMessage comum.

Política de compatibilidade de versão

Cada envelope carrega a PROTOCOL_VERSION do remetente, e ela é verificada no recebimento pelos dois lados:

  • Esperado: host e app rodam a mesma versão do protocolo (a mesma major do pacote nos dois lados).
  • Divergência: gera um aviso — o hook onVersionMismatch({ local, remote, type }) se você registrá-lo, ou um console.warn único por sessão. A mensagem ainda é processada: o bridge é forward-compatible por padrão, então uma divergência não derruba a comunicação sozinha.
  • Incompatibilidade real (mudança de shape de envelope/payload): exige bump coordenado — atualize o pacote nos dois lados na mesma janela. É para isso que a PROTOCOL_VERSION existe; use onVersionMismatch para telemetria e para decidir degradar funcionalidades quando as versões divergirem.

API

| Export | Propósito | | ---------------------------------------------------------- | ----------------------------------------------------------- | | createHost(opts) | Lado do host (sendConfig, navigate, emit, request). | | createApp(opts) | Lado do app (connect, navigate, emit, request). | | createIframeHostTransport / createIframeAppTransport | Web (postMessage). | | createWebViewHostTransport / createWebViewAppTransport | Mobile (RN WebView). | | createPostMessageTransport | Bloco de baixo nível para transportes web customizados. | | Transport | Implemente para qualquer outro ambiente. | | JsonValue, EventMap, RequestMap | Restrições de serialização e tipagem de eventos/RPC. |

Veja src/types.ts e src/protocol.ts para a superfície tipada completa.

Desenvolvimento

pnpm install
pnpm typecheck   # tsc --noEmit
pnpm test        # vitest
pnpm build       # tsdown → dist (esm + cjs + d.ts)
pnpm format      # prettier --write
pnpm change      # cria um changeset antes de um bump de versão

Licença

Veja LICENSE.md.