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

@conduito/port

v0.1.0

Published

The MessagePort transport for Conduito — Web Workers, SharedWorkers, MessageChannel and Node worker_threads, with transferables and respawn.

Readme

@conduito/port

O transporte do Conduito sobre MessagePort — Web Workers, SharedWorkers, MessageChannel e o worker_threads do Node. O mesmo protocolo, o mesmo Endpoint e as mesmas capabilities que o @conduito/frame roda sobre iframe, agora atravessando uma thread.

Filosofia: o contrato não muda quando o carrier muda. Uma capability declarada uma vez é chamada do mesmo jeito se o outro lado é um iframe cross-origin, um worker na mesma aba, ou uma thread do Node.

Roda nos dois mundos, com um código só. O pacote não importa node:worker_threads nem lib DOM: ele descreve o carrier estruturalmente e detecta em runtime se o objeto fala o estilo EventTarget (addEventListener, browser) ou EventEmitter (on/off, Node). O bundle publicado tem exatamente um import — @conduito/core.

pnpm add @conduito/port

Uso

Quem tem a porta (main thread)

import { connect } from "@conduito/port";

const worker = new Worker(new URL("./app.js", import.meta.url), {
  type: "module",
});

const { endpoint, connected } = connect(worker, {
  onConnect: async (endpoint) => {
    const { capabilities } = await endpoint.discover(); // o "db pull"
    renderSidebar(capabilities);
  },
});

const routes = await endpoint.request("routes", "list");

Quem está dentro (o worker)

import { listen } from "@conduito/port";
import { routesCap } from "./capabilities";

listen({
  onConnect(app) {
    app.expose(routesCap);
  },
});

Sem argumento, o listen adota o self do worker. No Node o parentPort não é global — passe-o:

import { parentPort } from "node:worker_threads";
listen(parentPort!, { onConnect: (app) => app.expose(routesCap) });

O que mais cabe aqui

| carrier | main thread | do outro lado | | --------------------- | ------------------------ | --------------------------------------------------------- | | Web Worker | connect(worker) | listen({ onConnect }) | | SharedWorker | connect(shared.port) | self.onconnect = e => listen(e.ports[0], { onConnect }) | | MessageChannel | connect(channel.port1) | listen(channel.port2, { onConnect }) | | Node worker_threads | connect(worker) | listen(parentPort!, { onConnect }) |

O handshake (e por que ele quase não aparece)

Os dois lados se anunciam: o connect pinga HANDSHAKE até ser respondido, e o listen manda um HELLO espontâneo assim que instala. Fecha por qualquer um dos dois caminhos, então a ordem em que você monta as pontas não importa — e o ping de segurança quase nunca chega a repetir.

O listen, diferente do @conduito/frame, não espera nada pra chamar seu onConnect: não há origem a validar, a porta já é a porta. Suas capabilities sobem primeiro, o anúncio vai depois.

O que fica enfileirado enquanto o par não respondeu não se perde: sai na conexão, em ordem. O timeout do próprio request continua limitando a espera, e a fila tem teto (queueLimit, default 100) — além dele o envio falha com EQUEUEFULL.

O worker morreu

Worker não recarrega como um iframe: ele morre (terminate(), um erro fatal) e você cria outro. O Endpoint é um só pra vida toda da conexão, então o que você registrou (handle, on, expose) sobrevive — o que morre é o outro lado.

const conn = connect(worker, {
  onConnect: (endpoint) => endpoint.notify("theme", "apply", theme), // a cada conexão
  onDisconnect: (reason, cause) => log(reason, cause), // "died" | "respawn" | "dispose"
});

// criou outro worker? aponte a conexão pra ele — mesmo endpoint, onConnect de novo
conn.rebind(new Worker(url, { type: "module" }));

// sabe que morreu e ainda não há substituto?
conn.disconnect(); // pendências rejeitadas (EDISCONNECTED), novos envios vão pra fila

Um request já enviado quando o worker caiu rejeita na hora com EDISCONNECTED — não depois do timeout inteiro. Ele pode ter executado; o transporte não repete.

Por padrão (watch: true) a conexão observa os eventos em que o carrier anuncia a própria morte — error (web e Node), exit e close — e desconecta sozinha, passando o erro em cause.

[!WARNING] No Node, um Worker sem nenhum listener de error derruba o processo. Com watch: true este pacote registra um — o que é o comportamento útil (você recebe em onDisconnect), mas silencia esse crash. Se você já trata error por conta própria, ou depende do crash, use watch: false.

Transferables

Transferir um buffer o esvazia do lado de quem enviou, então nada viaja assim por dedução — você marca:

import { transfer } from "@conduito/port";

await endpoint.request("image", "rotate", transfer({ pixels, angle }));
// `pixels` agora está detached aqui: foi movido, não copiado

// do lado do worker, o mesmo vale pra resposta
app.handle("image", "rotate", (p) => transfer(render(p)));

transfer(valor) devolve o próprio valor (dá pra embrulhar o argumento na chamada) e descobre sozinho o que é transferível lá dentro: ArrayBuffer, views tipadas (transfere o buffer delas), MessagePort, ImageBitmap, streams. Passe a lista explícita quando quiser controlar exatamente o que se move: transfer(payload, [pixels]). Sem a marca, o structured clone copia — que é o default seguro.

Segurança — o que muda em relação ao iframe

Não há origem aqui, e este pacote não finge que há. Um worker não tem origin pra checar: a confiança vem de quem instanciou o carrier — você passou a URL do script —, não de quem fala. O allowedOrigins obrigatório do @conduito/frame, que é a resposta à pergunta "quem é o par?", não tem análogo nesta topologia.

O que o transporte garante é o envelope: só uma Message válida do Conduito sobe pro Endpoint, e o resto do tráfego da porta é ignorado — então dividir a porta com outra biblioteca não quebra nada. As outras duas perguntas de segurança continuam iguais: o que está exposto é o que você registra com expose, e a sessão é decisão de cada handler.

Um SharedWorker atende várias páginas da mesma origem: cada e.ports[0] do onconnect é uma conexão distinta, e cada uma precisa do seu listen. O que você expõe num não vaza pro outro — são Endpoints separados.

API

| export | o quê | | ------------------------------------- | --------------------------------------------------------------------- | | connect(port, opts?) | dirige a ponta que você segura → Connection | | listen(opts) / listen(port, opts) | o lado de dentro → Listener (sem porta, adota o self do worker) | | transfer(value, list?) | marca o valor pra ser movido em vez de copiado | | transferablesOf(value) | a lista marcada num valor (para quem escreve transporte próprio) | | bind(port, type, handler) | assina um evento no estilo que o carrier falar; devolve o unsubscribe | | type PortLike | a forma estrutural que este pacote dirige | | type DisconnectReason | "respawn" | "died" | "dispose" |

ConnectOptions: { handshakeInterval?, queueLimit?, watch?, endpoint?, onConnect?, onDisconnect? } — endpoint são as EndpointOptions do core (defaultTimeout, onError). Connection: { endpoint, connected, rebind(port), disconnect(), dispose() }. Listener: { endpoint, dispose() }.

Erros codados que este transporte emite: EPORT (o alvo não tem postMessage), ETRANSFER (um primitivo foi marcado pra transferência) e EQUEUEFULL.

Internals: docs/ARCHITECTURE.md (no repositório).