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

event-contract

v0.3.1

Published

Type-safe, implementation-agnostic event contract framework.

Readme

Event Contract

Type-safe, implementation-agnostic event contract framework.

What is this for?

This is a tiny framework for type-safe event-based systems in TypeScript. It's built on the struggle that none of the event-based APIs in TypeScript are strict. Here's an example of a problem:

const target = new EventTarget()

target.addEventListener('greet', handler)
target.dispatchEvent(new CustomEvent('gret')) // Oops, a typo!

TypeScript will not warn or throw despite us making an obvious mistake above. The standard EventTarget API doesn't accept an events map generic either, leaving us no choice to make it more strict by normal means.

I am convinced that event-based system must be as strict as possible. You don't want to dispatch events you don't expect to handle. The data transferred in those events must be clearly defined. Type-safety must be achieved on build-time with TypeScript. This is precisely what this framework does.

While build-time type-safety is useful, it can be circumvented, opening the event contract to runtime errors. That's why this framework also comes with the support for a runtime schema validation for emitted data.

Getting started

Install

npm install event-contract

Transports

This framework operates on the concept of transports. Transports describe how to handle events and create subscriptions. Absolutely anything can be a transport: from the standard APIs like EventTarget and MessageChannel, to custom logic like communication with your database or a third-party service.

In this example, we will implement a custom transport over EventTarget. Each transport is described using two methods:

  • push() describes what to do when a new event is emitter;
  • subscribe() describes how to handle new subscriptions.

In the context of EventTarget, we handle push() by target.dispatchEvent(), and we handle subscribe() by target.addEventListener(). Here's the final transport implementation:

const target = new EventTarget()

new EventContract({
  transport: {
    push(type, data) {
      // Translate pushing a new event to dispatching
      // a "MessageEvent" on this event target.
      target.dispatchEvent(new MessageEvent(type, { data }))
    },
    subscribe(type, next) {
      const handler = (event: Event) => {
        if (event instanceof MessageEvent) {
          next(event.data)
        }
      }

      // Add a new listener when a subscription occurs.
      target.addEventListener(type, handler)

      return () => {
        // Unsubscribe from this by removing the listener.
        target.removeEventListener(type, handler)
      }
    },
  },
})

The EventTarget API is a great choice because it's present in both browser and Node.js, meaning that we can now use that contract in those environments.

Note that this is an example implementation. This framework exports a set of Default transfers that you should use for event contracts over standard JavaScript API.

Events map

(Recommended) Combined

We highly recommend describing the events of your contract using the schema option of the EventContract constructor.

import { z } from 'zod'
import { EventContract, eventTargetTransport } from 'event-contract'

const contract = new EventContract({
  transport: eventTargetTransport(),
  schema: {
    greet: z.string(),
  },
})

contract.push('greet', 'John') // ✅
contract.push('greet', 123) // ❌

Created event contract automatically infers event type and payload types from the Zod schema you provide. This gives you both build-time and runtime safety, end-to-end.

Type-only

You can opt-out from runtime validation by not providing the schema property to your event contract. In that case, you can still annotate expected event types and their payloads by providing an EventsMap generic to the EventContract constructor:

type MyEvents = {
  greet: string
}

const contract = new EventsContract<MyEvents>({ transport })
contract.subscribe('greet', (name) => name.toUpperCase()

contract.push('greet', 'John') // ✅ OK!
contract.push('greet', 123) // ❌ "number" is not assignable to type "string"

This approach doesn't provide any runtime data validation so we highly recommend using a Combined events map.


Default transfers

This framework comes with a list of default transfers that implement event contract using various built-in APIs.

  • eventTargetTransport()
  • broadcastChannelTransport()

Each built-in transport is a function that returns the event contract options. Provide those options to the EventContract constructor to use that transport.

import { EventContract, eventTargetTransport } from 'event-contract'

const contract = new EventContract<{ greet: string }>({
  transport: eventTargetTransport(),
})

API

EventContract

type Events = {
  greet: string
}

const contract = new EventContract<Events>({
  transport: {
    push(type, data) {
      // Describe how events should be emitted.
    },
    subscribe(type, next) {
      // Attach a listener when a subscription occurs.

      return () => {
        // Describe how to unsubscribe from this subscription.
      }
    },
  },
})

EventContract.subscribe()

contract.subscribe('greet', (name) => {
  console.log(`hello, ${name}`)
})

EventContract.push()

contract.push('greet', 'John')

EventContract.unsubscribe()

Unsubscribes from the established subscriptions.

When called without any arguments, the .unsubscribe() method removes all active subscriptions for all event types. This is useful for freeing memory when you no longer need this contract.

contract.unsubscribe()

If you provide an event type, all the subscriptions of that event type will be removed.

contract.unsubscribe('greet')

You can also provide both the event type and a specific listener function. In that case, only that given listener will be removed.

contract.unsubscribe('greet', listener)

Note that every subscription also returns a function that you can use the unsubscribe the respective listener directly:

const unsubscribe = contract.subscribe('greet', listener)
unsubscribe()

Recipes

Handling an event once

You may have noticed that there isn't something like a .subscribeOnce() on the contract. Instead, in order to handle a certain event once, you have to unsubscribe that handler explicitly:

contract.subscribe('greet', (name) => {
  // Unsubscribe from this handler.
  // Note that all subscriptions are bound to themselves,
  // allowing you to reference them as "this".
  contract.unsubscribe('greet', this)
})

Alternatively, you can use the unsubscribe function returned from every subscription to achieve the same result:

const unsubscribe = contract.subscribe('greet', (name) => {
  unsubscribe()
})