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

@diegesis/events

v0.2.1

Published

Mature, schema-validated event & hook bus for Diegesis — cross-VTT interoperability and browser-extension friendly.

Readme

@diegesis/events

Mature, schema-validated event & hook bus for Diegesis — cross-VTT interoperability and browser-extension friendly.

The foundation of the Diegesis hook system: typed, validated events and extensible hook pipelines that stay drop-in simple and friendly to browser extensions and injected scripts.

  • Events (notifications, fire-and-forget) — powered by mitt: typed, wildcard-capable.
  • Hooks (interception / pipelines) — powered by tapable: eight strategies from sync to asyncParallelBail.
  • Validation — every event/hook payload is validated with Valibot schemas (v.GenericSchema accepted directly).
  • IDs — UUID v7 (time-ordered, sortable) for every event/correlation id.
  • Bridge — exposes the bus to globalThis.__DIEGESIS_EVENTS__ and dispatches DOM CustomEvents, so content scripts and extensions can participate with plain addEventListener.
  • Broadcast — optional cross-tab propagation via BroadcastChannel.
  • Robust — a throwing handler never crashes the bus; errors are isolated and observable.
  • SSR-safe (no window/document access at import time), ESM + CJS, tree-shakeable.

Installation

npm install @diegesis/events

Quick start

import * as v from 'valibot';
import { createBus, defineContract } from '@diegesis/events';

const contract = defineContract({
  namespace: 'dice',
  events: {
    rolled: v.object({ result: v.number() }),
  },
  hooks: {
    beforeRoll: { strategy: 'syncWaterfall', schema: v.object({ sides: v.number() }) },
  },
});

const bus = createBus(contract);

bus.on('rolled', (payload, meta) => {
  console.log('rolled', payload.result, meta.id); // meta.id is a UUID v7
});

bus.tap('beforeRoll', 'physics', (ctx) => ctx); // waterfall taps return the (transformed) value

const result = bus.call('beforeRoll', { sides: 20 }); // → { sides: 20 }
bus.emit('rolled', { result: 42 }); // validated against the schema

Freeform usage without a contract also works:

import { createBus } from '@diegesis/events';

const bus = createBus();
const off = bus.on('scene:ready', (payload, meta) => {
  console.log(meta.id, meta.timestamp);
});
bus.emit('scene:ready', { id: 'forest' });
off();

API

createBus(contract?, options?) / new EventBus(contract?, options?)

contract: { namespace?, events?, hooks? } — events maps name → Valibot schema; hooks maps name → { strategy, schema? }. Build typed contracts with defineContract({ namespace, events, hooks }).

| Option | Type | Default | Description | | --- | --- | --- | --- | | validate | 'throw' \| 'warn' \| 'off' | 'throw' | Payload validation behaviour. | | unknownEvents | 'allow' \| 'reject' | 'allow' | Reject emits for events not in the contract. | | bridge | boolean \| BridgeOptions | false | Expose the bus to extensions/scripts. | | broadcast | boolean \| BroadcastOptions | false | Cross-tab BroadcastChannel. | | debug | boolean \| ((...a) => void) | false | Debug logging. | | sourceId | string | uuid v7 | Unique instance id used for broadcast de-dup. |

Events (notify)

bus.on(name, (payload, meta) => {});        // subscribe → returns unsubscribe fn
bus.once(name, (payload, meta) => {});      // fire once
bus.off(name, handler);                     // remove a handler
bus.onAny((name, payload, meta) => {});     // observe every event
bus.emit(name, payload);                    // → EventMeta (contains the uuid v7 id)
bus.onError((err, ctx) => {});              // observe handler/validation errors
  • emit is synchronous and returns the EventMeta (so you get the event id for correlation).
  • A handler that throws is isolated: it is reported via onError and the remaining handlers still run.

Hooks (pipelines)

Hooks are extensible pipeline points where plugins can intercept, transform, or short-circuit a process.

bus.tap(name, 'plugin-name', (value) => value);           // sync tap
bus.tapPromise(name, 'plugin-name', async (value) => {}); // async tap
bus.call(name, value);        // invoke a SYNC hook (sync / syncBail / syncWaterfall)
bus.callAsync(name, value);   // invoke ANY hook (required for async* strategies)

Hook inputs are validated against the hook's schema before any tap runs.

Runtime registration

bus.registerEvent('spawned', v.object({ tokenId: v.string() }));
bus.registerHook('damage', { strategy: 'syncWaterfall', schema: v.object({ amount: v.number() }) });

Validation

Validation mode is set per-bus via validate: 'throw' | 'warn' | 'off' (default 'throw').

  • Events without a schema and hooks without a schema are never validated (freeform-friendly).
  • On 'throw', invalid payloads raise an EventValidationError listing every issue.

Errors

EventBusError, EventValidationError, UnknownEventError, UnknownHookError, HookError — all carry a typed code.

Hook strategies

| Strategy | Runs | Behaviour | | ------------------------ | ------ | -------------------------------------------------------------------------- | | sync | sync | All taps run; return values ignored. | | syncBail | sync | Stops at the first tap returning non-undefined; returns it. | | syncWaterfall | sync | Each tap receives the previous return and returns the next value. | | asyncSeries | async | Taps run sequentially in registration order. | | asyncSeriesBail | async | Sequential; stops at the first resolved non-undefined. | | asyncSeriesWaterfall | async | Sequential; each tap receives the previous resolved value. | | asyncParallel | async | All taps run concurrently. | | asyncParallelBail | async | Concurrent; resolves with the first non-undefined. |

Browser extensions & external scripts (the bridge)

Enable the bridge to let content scripts, page scripts, and browser extensions talk to the bus using only standard APIs. No build step required for them.

const bus = createBus(contract, { bridge: true });

This does two things:

1. A global handle at globalThis.__DIEGESIS_EVENTS__ (constant: GLOBAL_KEY):

// from a content script, page script, or extension
const bus = window.__DIEGESIS_EVENTS__.get('dice');
bus.on('rolled', (payload, meta) => console.log(payload, meta.id));
bus.emit('rolled', { result: 42 }); // validated against the contract schema
bus.events(); // → ['rolled']

2. DOM CustomEvents dispatched on document (the most reliable channel across Chrome content-script isolated worlds) with the namespaced name ${namespace}:${event} and event.detail = { payload, meta }:

// works from any content script / injected script, no SDK needed
document.addEventListener('dice:rolled', (e) => {
  console.log(e.detail.payload, e.detail.meta.id);
});

The default bridge target is document when available (for maximum content-script compatibility), falling back to globalThis. Override with bridge: { target: myTarget }.

External emit/tap calls are validated just like internal ones, so the bus stays reliable even when untrusted scripts participate.

Cross-tab broadcast

const bus = createBus(contract, { broadcast: true }); // channel defaults to `diegesis:<namespace>`

Emits are mirrored to other tabs/windows on the same origin via BroadcastChannel. Received messages are re-dispatched locally but never re-broadcast (no loops). Inbound payloads that fail validation are reported via onError and dropped (a misbehaving peer can never crash your tab).

Because the bus is VTT-agnostic and namespaced, you can wire several adapters against the same bus to translate between platforms.


Part of diegesis-sdk. MIT.