nano-pubsub

A zero-dependency, sub-0.5kb TypeScript pub/sub utility for type-safe in-app event handling.

Library
npm
v3.0.0
16stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
27/100Needs Attention
Development Activity0
Maintenance20
Community16
Maturity60
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
54/100Fair
Architecture70
Code Quality60
Innovation60
Learning Curve25

nano-pubsub is an extremely small (under 0.5kb) publish/subscribe library for JavaScript and TypeScript applications. It exposes a single factory function, createPubSub, that returns an object with publish and subscribe methods, letting you set up a fully typed event channel with a handful of lines of code and zero runtime dependencies.

The library targets situations where a full event-emitter package or state-management framework would be overkill — in-component communication, decoupling modules within an app, or building simple typed message buses. Its generic Message type parameter gives subscribers full type inference for the values passed to publish, and each call to subscribe returns its own unsubscribe function for straightforward, per-listener cleanup.

What You Get

  • A createPubSub<Message>() factory that returns a typed publish/subscribe channel
  • Full TypeScript generics so subscribers receive strongly-typed event payloads
  • An unsubscribe() function returned from every subscribe() call for granular, per-listener cleanup
  • Dual ESM/CJS build output (dist/index.js and dist/index.cjs) with bundled type declarations

Common Use Cases

  • Decoupling UI components or modules that need to communicate without direct references
  • Building a lightweight in-app event bus for cross-cutting concerns like analytics or logging
  • Replacing ad-hoc callback arrays with a typed, reusable subscribe/publish pattern
  • Adding a small internal pub/sub layer inside a library where a full event-emitter dependency isn’t justified

Under The Hood

Architecture The entire module is a single closure-scoped factory: createPubSub keeps a subscribers object keyed by an incrementing numeric id, subscribe adds an entry and returns an unsubscribe closure that deletes it, and publish iterates the object with for...in to invoke every current subscriber. There is no internal layering, no dependency injection, and no external state to manage — the whole abstraction is this one object literal, so changing the subscriber-storage strategy would mean rewriting the module in a single pass rather than touching an isolated layer.

Tech Stack Written in TypeScript and built with @sanity/pkg-utils (v5) into dual ESM (dist/index.js) and CommonJS (dist/index.cjs) outputs with bundled .d.ts declarations; rimraf clears the dist directory before each build. Tests run directly against the TypeScript source via the tap test runner with its @tapjs/typescript preset. The package declares zero runtime dependencies and targets Node >=18 per its engines field — the published bundle itself is the entire runtime stack.

Code Quality test.ts covers five focused cases directly against src/index.ts: basic delivery, unsubscribe behavior, multi-argument tuple payloads, duplicate-subscriber call counting, and out-of-order subscribe/unsubscribe sequencing. Naming is terse and consistent (subscribe/publish/unsubscribe), and the generic Message type parameter gives compile-time safety to publish/subscribe payloads. Formatting is enforced via a checked-in .prettierrc, but no CI workflow file or linter config is present in the repository, and error handling is minimal — reasonably so, since the module has no fallible operations to guard.

API Design The public surface is a single default export, createPubSub<Message>(), returning exactly two methods. Returning a distinct unsubscribe closure from each subscribe() call — rather than requiring callers to retain a function reference to pass into a shared removeListener — removes a common source of memory-leak bugs in hand-rolled emitters. There’s no wildcard matching, namespacing, or async iteration the way larger event-emitter libraries offer, but for its narrow scope the API requires essentially no boilerplate and infers its generic type from usage in the common case.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search