Coder Social home page Coder Social logo

dashlane / ts-event-bus Goto Github PK

View Code? Open in Web Editor NEW
138.0 21.0 15.0 493 KB

๐Ÿ“จ Distributed messaging in TypeScript

Home Page: https://www.npmjs.com/package/ts-event-bus

License: Apache License 2.0

TypeScript 99.53% JavaScript 0.47%
typescript eventbus websocket distributed-messaging

ts-event-bus's Introduction

ts-event-bus

by Dashlane

Build Status Dependency Status

Caution

This package is no longer maintained. It is strongly advised against using it in any new project.

Distributed messaging in Typescript

ts-event-bus is a lightweight distributed messaging system. It allows several modules, potentially distributed over different runtime spaces to communicate through typed messages.

Getting started

Declare your events

Using ts-event-bus starts with the declaration of the interface that your components share:

// MyEvents.ts
import { slot, Slot } from "ts-event-bus";

const MyEvents = {
  sayHello: slot<string>(),
  getTime: slot<null, string>(),
  multiply: slot<{ a: number; b: number }, number>(),
  ping: slot<void>(),
};

export default MyEvents;

Create EventBus

Your components will then instantiate an event bus based on this declaration, using whatever channel they may want to communicate on. If you specify no Channel, it means that you will exchange events in the same memory space.

For instance, one could connect two node processes over WebSocket:

// firstModule.EventBus.ts
import { createEventBus } from "ts-event-bus";
import MyEvents from "./MyEvents.ts";
import MyBasicWebSocketClientChannel from "./MyBasicWebSocketClientChannel.ts";

const EventBus = createEventBus({
  events: MyEvents,
  channels: [new MyBasicWebSocketClientChannel("ws://your_host")],
});

export default EventBus;
// secondModule.EventBus.ts
import { createEventBus } from "ts-event-bus";
import MyEvents from "./MyEvents.ts";
import MyBasicWebSocketServerChannel from "./MyBasicWebSocketServerChannel.ts";

const EventBus = createEventBus({
  events: MyEvents,
  channels: [new MyBasicWebSocketServerChannel("ws://your_host")],
});

Usage

Once connected, the clients can start by using the slots on the event bus

// firstModule.ts
import EventBus from './firstModule.EventBus.ts'

// Slots can be called with a parameter, here 'michel'
EventBus.say('michel', 'Hello')

// Or one can rely on the default parameter: here DEFAULT_PARAMETER
// is implicitely used.
EventBus.say('Hello')

// Triggering an event always returns a promise
EventBus.say('michel', 'Hello').then(() => {
    ...
})

EventBus.getTime().then((time) => {
    ...
})

EventBus.multiply({a: 2, b: 5 }).then((result) => {
    ...
})

EventBus.ping()
// secondModule.ts
import EventBus from "./secondModule.EventBus.ts";

// Add a listener on the default parameter
EventBus.ping.on(() => {
  console.log("pong");
});

// Or listen to a specific parameter
EventBus.say.on("michel", (words) => {
  console.log("michel said", words);
});

// Event subscribers can respond to the event synchronously (by returning a value)
EventBus.getTime.on(() => new Date().toString);

// Or asynchronously (by returning a Promise that resolves with the value).
EventBus.multiply.on(
  ({ a, b }) =>
    new Promise((resolve, reject) => {
      AsynchronousMultiplier(a, b, (err, result) => {
        if (err) {
          return reject(err);
        }
        resolve(result);
      });
    })
);

Calls and subscriptions on slots are typechecked

EventBus.multiply({a: 1, c: 2}) // Compile error: property 'c' does not exist on type {a: number, b: number}

EventBus.multiply.on(({a, b}) => {
    if (a.length > 2) { // Compile error: property 'length' does not exist on type 'number'
        ...
    }
})

Lazy callbacks

Slots expose a lazy method that will allow you to call a "connect" callback when a first client connects to the slot, and a "disconnect" callback when the last client disconnect.

Remote or local clients are considered equally. If a client was already connected to the slot at the time when lazy is called, the "connect" callback is called immediately.

const connect = (param) => {
  console.log(
    `Someone somewhere has begun listening to the slot with .on on ${param}.`
  );
};

const disconnect = (param) => {
  console.log(`No one is listening to the slot anymore on ${param}.`);
};

const disconnectLazy = EventBus.ping.lazy(connect, disconnect);

const unsubscribe = EventBus.ping().on(() => {});
// console output: 'Someone somewhere has begun listening to the slot with .on on $_DEFAULT_$.'

unsubscribe();
// console output: 'No one is listening to the slot anymore on $_DEFAULT_$.'

const unsubscribe = EventBus.ping().on("parameter", () => {});
// console output: 'Someone somewhere has begun listening to the slot with .on on parameter.'

unsubscribe();
// console output: 'No one is listening to the slot anymore on parameter.'

// Remove the callbacks.
// "disconnect" is called one last time if there were subscribers left on the slot.
disconnectLazy();

Buffering

When the eventBus is created with channels, slots will wait for all transports to have registered callbacks before triggering.

This buffering mechanism can be disabled at the slot level with the noBuffer config option:

const MyEvents = {
  willWait: slot<string>(),
  wontWait: slot<string>({ noBuffer: true }),
};

Auto-reconnection

In order to re-establish a lost connection when triggering an event a Channel needs to implement the autoReconnect method. See example: RuntimeConnect It's also possible to fine tune and deactivate this feature on a per-slot basis :

const MyEvents = {
  willAutoReconnect: slot<string>(),
  wontNotAutoReconnect: slot<string>({ autoReconnect: false }),
};

Syntactic sugar

You can combine events from different sources.

import { combineEvents } from "ts-event-bus";
import MyEvents from "./MyEvents.ts";
import MyOtherEvents from "./MyOtherEvents.ts";

const MyCombinedEvents = combineEvents(MyEvents, MyOtherEvents);

export default MyCombinedEvents;

Using and Implementing Channels

ts-event-bus comes with an abstract class GenericChannel. To implement your own channel create a new class extending GenericChannel, and call the method given by the abstract class: _connected(), _disconnected(), _error(e: Error) and _messageReceived(data: any).

Basic WebSocket Channel example:

import { GenericChannel } from "ts-event-bus";

export class MyBasicWebSocketChannel extends GenericChannel {
  private _ws: WebSocket | null = null;
  private _host: string;

  constructor(host: string) {
    super();
    this._host = host;
    this._init();
  }

  private _init(): void {
    const ws = new WebSocket(this._host);

    ws.onopen = (e: Event) => {
      this._connected();
      this._ws = ws;
    };

    ws.onerror = (e: Event) => {
      this._ws = null;
      this._error(e);
      this._disconnected();
      setTimeout(() => {
        this._init();
      }, 2000);
    };

    ws.onclose = (e: CloseEvent) => {
      if (ws === this._ws) {
        this._ws = null;
        this._disconnected();
        this._init();
      }
    };

    ws.onmessage = (e: MessageEvent) => {
      this._messageReceived(e.data);
    };
  }
}

Examples

ts-event-bus's People

Contributors

aktanee avatar alexandre-herve avatar bastiengranger avatar dependabot[bot] avatar fellowseb avatar highfivedenis avatar lguychard avatar mikescops avatar pyrho avatar xavier-besson avatar ziir avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ts-event-bus's Issues

In the `WebSocketClientChannel` example, ECONNREFUSED crashes the app

If it cannot connect to the server, the app will crash because there is no error event handler.

The error handler in the example is only attached after open.

    let ws: WebSocket | null = null
    try {
      ws = new WebSocket(this._host)
    } catch (err) {
      setTimeout(() => this._init, 500)
    }
    ws.on('error', (err) => { <------- added
      console.log({err})
    })
    ;(ws as WebSocket).on('open', () => {
      this._ws = ws as WebSocket
      this._ws.on('close', reinit)
      this._ws.on('error', e => {
        this._error(e)
        reinit()
      })
Error: connect ECONNREFUSED 127.0.0.1:55555
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1142:16)
Emitted 'error' event on WebSocket instance at:
    at ClientRequest.<anonymous> (/Uxxx/node_modules/.pnpm/[email protected]/node_modules/ws/lib/websocket.js:579:15)
    at ClientRequest.emit (events.js:315:20)
    at Socket.socketErrorListener (_http_client.js:463:9)
    at Socket.emit (events.js:315:20)
    at emitErrorNT (internal/streams/destroy.js:100:8)
    at emitErrorCloseNT (internal/streams/destroy.js:68:3)
    at processTicksAndRejections (internal/process/task_queues.js:84:21) {

Feature Request: Add Unsubscribe All

Can you add a unsubscribeAll or offAll method on the event bus so all listeners can be removed with one call?

const EventBus = createEventBus({ events: MyEvents });

EventBus.ping.on(() => { console.log("pong"); });
EventBus.say.on("michel", (words) => { console.log("michel said", words); });

// Add a unsubscribe all method
EventBus.unsubscribeAll();

Message forwarding

Is it possible to forward messages?

Use case

Chrome Extension <-- (stdio) --> Chrome Native Messaging Host <-- (websockets) --> Electron App

My Electron app wants to call a method of the Chrome Extension, which needs to communicate via the messaging host.

What is the best approach for this scenario?

Question about events with no listeners

Hi! First of all, thanks for this nice lib :) I have a question: what happens, when you call an event and there are no listeners on its slot? It would be useful to describe that in the readme.

From what I tested, the promise returned by the event call just waits indefinitely. Is this your designed behavior, so a listener can still connect while the message is awaiting on the channel? If so, a nice feature would be to add an "instant delivery" mode, configured e.g. in an optional params object to the slot<Type>() function, where the call would fail (and reject the promise) if there are no connected listeners on that slot.

[CHORE]: Split `Channels` from the main lib bundle.

@pyrho added a ChunkedChannel recently to the lib which is cool but made me think about the separation of concern in the lib.

As we may want to add more and more channel in the future, it may be ideal to split Channels from the core of the lib.

That way we could import * 'from ts-event-bus' for the core.
And import * from 'ts-event-bus/ChunkedChannel or import * from 'ts-event-bus/Channels/ChunkedChannel for channels

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.