@solid-primitives/event-bus

A collection of SolidJS primitives providing various features of a pubsub/event-emitter/event-bus.

Usage no npm install needed!

<script type="module">
  import solidPrimitivesEventBus from 'https://cdn.skypack.dev/@solid-primitives/event-bus';
</script>

README

@solid-primitives/event-bus

lerna size size stage

A collection of SolidJS primitives providing various features of a pubsub/event-emitter/event-bus:

  • createSimpleEmitter - Very minimal interface for emiting and receiving events. Good for parent-child component communication.
  • createEmitter - Provides all the base functions of an event-emitter, plus additional functions for managing listeners, it's behavior could be customized with an config object. Good for advanced usage.
  • createEventBus - Extends createEmitter. Additionally it provides a signal accessor function with last event's value.
  • createEventStack - Extends createEmitter. Provides the emitted events in a list/history form, with tools to manage it.
  • createEventHub - Provides helpers for using a group of emitters.

Installation

npm install @solid-primitives/event-bus
# or
yarn add @solid-primitives/event-bus

createSimpleEmitter

Very minimal interface for emiting and receiving events. Good for parent-child component communication.

How to use it

import { createSimpleEmitter } from "@solid-primitives/event-bus";

// accepts up-to-3 genetic payload types
const [listen, emit, clear] = createSimpleEmitter<string, number, boolean>();

// can be used without payload type, if you don't want to send any
createSimpleEmitter();

listen((a, b, c) => console.log(a, b, c));

emit("foo", 123, true);

// clear all listeners
clear();
// listeners will also be cleared onCleanup automatically

Types

Check out shared types.

function createSimpleEmitter<A0 = void, A1 = void, A2 = void>(): [
  listen: GenericListen<[A0, A1, A2]>,
  emit: GenericEmit<[A0, A1, A2]>,
  clear: ClearListeners
];

createEmitter

Provides all the base functions of an event-emitter, plus additional functions for managing listeners, it's behavior could be customized with an config object. Good for advanced usage.

How to use it

Creating Emitter

import { createEmitter } from "@solid-primitives/event-bus";

// accepts up-to-3 genetic payload types
const emitter = createEmitter<string, number, boolean>();

// can be used without payload type, if you don't want to send any
createEmitter();

// emitter can be destructured:
const { listen, emit, has, clear } = emitter;

Emiting & Listening to events

const listener = (a, b, c) => console.log(a, b, c);
emitter.listen(listener);

emitter.emit("foo", 123, true);

emitter.remove(listener);
emitter.has(listener); // false

// pass true as a second argument to protect the listener
emitter.listen(listener, true);
emitter.remove(listener);
emitter.has(listener); // true

// clear all listeners
emitter.clear();
// listeners will also be cleared onCleanup automatically

Emitter Config

const { listen, has, remove, emit } = createEmitter<string>({
  beforeEmit: event => {...},
  emitGuard: (emit, payload) => allowedEmit && emit(), // emit('foo') to emit different value
  removeGuard: (remove, listener) => allowedRemove && remove()
});

See the tests for better usage reference.

Types

Check out shared types.

function createEmitter<A0 = void, A1 = void, A2 = void>(
  config: EmitterConfig<A0, A1, A2> = {}
): Emitter<A0, A1, A2>;

type Emitter<A0 = void, A1 = void, A2 = void> = {
  listen: GenericListenProtect<[A0, A1, A2]>;
  emit: GenericEmit<[A0, A1, A2]>;
  remove: Remove<A0, A1, A2>;
  clear: ClearListeners;
  has: (listener: GenericListener<[A0, A1, A2]>) => boolean;
};

type EmitterConfig<A0 = void, A1 = void, A2 = void> = {
  emitGuard?: EmitGuard<A0, A1, A2>;
  removeGuard?: RemoveGuard<GenericListener<[A0, A1, A2]>>;
  beforeEmit?: GenericListener<[A0, A1, A2]>;
};

createEventBus

Extends createEmitter. Additionally it provides a signal accessor function with last event's value.

How to use it

Creating EventBus

import { createEventBus } from "@solid-primitives/event-bus";

const bus = createEventBus<string>();

// can be destructured:
const { listen, emit, has, clear, value } = bus;

Emitting & listening to events

const listener = (event, previous) => console.log(event, previous);
bus.listen(listener);

bus.emit("foo");

bus.remove(listener);
bus.has(listener); // false

// pass true as a second argument to protect the listener
bus.listen(listener, true);
bus.remove(listener);
bus.has(listener); // true

// clear all listeners
bus.clear();
// listeners will also be cleared onCleanup automatically

Last Value signal

// last event is be available as a signal
bus.value(); // => string | undefined

// pass initial value to config to remove "undefined" from the type
createEventBus({
  value: "initial"
});

bus.value(); // => string

EventBus Config

createEventBus<string>({
  beforeEmit: event => console.log(event),
  emitGuard: (emit, event, prev) => allowedEmit && emit(), // emit('foo') to emit different value,
  removeGuard: (remove, listener) => allowedRemove && remove(),
  value: "Initial Value"
});

See the tests for better usage reference.

Types

Check out shared types.

// Initial value was NOT provided
function createEventBus<Event>(
  config?: EmitterConfig<Event, Event | undefined>
): EventBus<Event, Event | undefined>;
// Initial value was provided
function createEventBus<Event>(
  config: EmitterConfig<Event, Event> & {
    value: Event;
  }
): EventBus<Event, Event>;

type EventBusListener<Event, V = Event | undefined> = GenericListener<[Event, V]>;
type EventBusListen<Event, V = Event | undefined> = ListenProtect<Event, V>;

type EventBusRemove<Event, V = Event | undefined> = (
  listener: EventBusListener<Event, V>
) => boolean;

type EventBus<Event, V = Event | undefined> = {
  remove: EventBusRemove<Event, V>;
  listen: EventBusListen<Event, V>;
  emit: GenericEmit<[Event]>;
  clear: ClearListeners;
  has: (listener: EventBusListener<Event, V>) => boolean;
  value: Accessor<V>;
};

createEventStack

Extends createEmitter. Provides the emitted events in a list/history form, with tools to manage it.

How to use it

Creating the event bus

import { createEventStack } from "@solid-primitives/event-bus";

// 1. event type has to be an object
// be what you emit will be added to the value stack
const bus = createEventStack<{ message: string }>();

// 2. provide event type, value type, and toValue parsing function
// value type has to be an object
const bus = createEventStack<string, { text: string }>({
  toValue: e => ({ text: e })
});

// can be destructured:
const { listen, emit, has, clear, value } = bus;

Listening & Emitting

const listener: EventStackListener<{ text: string }> = (event, stack, removeValue) => {
  console.log(event, stack);
  // you can remove the value from stack
  removeValue();
};
bus.listen(listener);

bus.emit("foo");

bus.remove(listener);
bus.has(listener); // false

// pass true as a second argument to protect the listener
bus.listen(listener, true);
bus.remove(listener);
bus.has(listener); // true

Event Stack

// a signal accessor:
bus.stack() // => { text: string }[]

bus.removeFromStack(value) // pass a reference to the value

bus.setStack(stack => stack.filter(item => {...}))

createEventStack Config

createEventStack<string, { text: string }>({
  beforeEmit: (value, stack, remove) => console.log(value, stack),
  emitGuard: (emit, text) => allowEmit && emit(), // emit('foo') to emit different value
  removeGuard: (remove, listener) => allowRemove && remove(),
  toValue: e => ({ text: e })
});

Types

Check out shared types.

// Overload 0: "toValue" was not passed
function createEventStack<E extends object>(config?: Config<E, E>): EventStack<E, E>;
// Overload 1: "toValue" was set
function createEventStack<E, V extends object>(
  config: Config<E, V> & {
    toValue: (event: E, stack: V[]) => V;
  }
): EventStack<E, V>;

type EventStackListener<V> = (event: V, stack: V[], removeFromStack: Fn) => void;

type EventStack<E, V = E> = Modify<
  Emitter<V, V[], Fn>,
  {
    value: Accessor<V[]>;
    stack: Accessor<V[]>;
    setStack: Setter<V[]>;
    removeFromStack: (value: V) => boolean;
    emit: GenericEmit<[E]>;
  }
>;
type Config<E, V> = {
  length?: number;
  emitGuard?: EmitterConfig<E>["emitGuard"];
  removeGuard?: EmitterConfig<V, V[], Fn>["removeGuard"];
  beforeEmit?: EmitterConfig<V, V[], Fn>["beforeEmit"];
};

createEventHub

Provides helpers for using a group of emitters.

Can be used with createEmitter, createEventBus, createEventStack.

How to use it

Creating EventHub

import { createEventHub } from "@solid-primitives/event-bus";

// by passing an record of Channels
const hub = createEventHub({
  busA: createEmitter<void>(),
  busB: createEventBus<string>(),
  busC: createEventStack<{ text: string }>()
});

// by passing a function
const hub = createEventHub(bus => ({
  busA: bus<number>(),
  busB: bus<string>(),
  busC: createEventStack<{ text: string }>()
}));

// hub can be destructured
const { busA, busB, on, off, listen, emit, clear } = hub;

Listening & Emitting

// using hub methods:
hub.on("busA", e => {});
hub.on("busB", e => {});

hub.emit("busA", 0);
hub.emit("busB", "foo");

// using emitters
hub.busA.listen(e => {});
hub.busA.emit(1);

hub.busB.listen(e => {});
hub.busB.emit("bar");

// global listener - listens to all channels
hub.listen((name, e) => {});

Accessing values

If a emitter returns an accessor value, it will be available in a .store store.

hub.store.myBus;
// same as
hub.myBus.value();

Types

Check out shared types and createEventHub source.

function createEventHub<ChannelMap extends Record<string, EventHubChannel>>(
  defineChannels: ((bus: typeof createEventBus) => ChannelMap) | ChannelMap
): EventHub<ChannelMap>;
/**
 * Required interface of a Emitter/EventBus, to be able to be used as a channel in the EventHub
 */
interface EventHubChannel {
  remove: (fn: (...payload: any[]) => void) => boolean;
  listen: (listener: (...payload: any[]) => void, protect?: boolean) => Unsubscribe;
  emit: (...payload: any[]) => void;
  clear: ClearListeners;
  value: Accessor<any>;
}
type EventHub<ChannelMap extends Record<string, EventHubChannel>> = ChannelMap & {
  on: EventHubOn<ChannelMap>;
  off: EventHubOff<ChannelMap>;
  emit: EventHubEmit<ChannelMap>;
  clear: (event: keyof ChannelMap) => void;
  clearAll: ClearListeners;
  listen: (listener: EventHubListener<ChannelMap>, protect?: boolean) => Unsubscribe;
  remove: (listener: EventHubListener<ChannelMap>) => void;
  clearGlobal: ClearListeners;
  store: ValueMap<ChannelMap>;
};

EventBus Utils

toPromise

Turns a stream-like listen function, into a promise resolving when the first event is captured.

import { toPromise } from "@solid-primitives/event-bus";

const emitter = createEmitter<string>();
const event = await toPromise(emitter.listen);

// can be used together with raceTimeout from @solid-primitives/utils
import { raceTimeout } from "@solid-primitives/utils";
try {
  const event = await raceTimeout(toPromise(emitter.listen), 2000, true, "event was too slow");
  // if event is quicker:
  event; // => string
} catch (err) {
  // if timeouts:
  console.log(err); // => "event was too slow"
}

once

Listen to any EventBus/Emitter, but the listener will automatically unsubscribe on the first captured event. So the callback will run only once.

import { once } from "@solid-primitives/event-bus";

const { listen, emit } = createEmitter<string>();
const unsub = once(listen, event => console.log(event));

emit("foo"); // will log "foo" and unsub

emit("bar"); // won't log

toEffect

Wraps emit calls inside a createEffect. It causes that listeners execute having an reactive owner available. It allows for usage of effects, memos and other primitives inside listeners, without having to create a synthetic root.

import { toEffect } from "@solid-primitives/event-bus";

const { listen, emit } = createEmitter();
const emitInEffect = toEffect(emit);

// owner is needed for creating computations like createEffect
listen(() => console.log(getOwner()));

// ...sometime later (after root initiation):
emit(); // listener will log `null`
emitInEffect(); // listener will log an owner object

Demo

https://codesandbox.io/s/solid-primitives-event-bus-6fp4h?file=/index.tsx

Changelog

Expand Changelog

0.0.100

Initial release as a Stage-2 primitive.

0.0.112

Minor improvement