Coder Social home page Coder Social logo

vitaly-z / tangle Goto Github PK

View Code? Open in Web Editor NEW

This project forked from stateful/tangle

1.0 0.0 0.0 2.55 MB

JavaScript state synchronization and event handling for various of different sandbox environments, e.g. worker threads, web workers, iframes, Visual Studio Code Webviews etc.

License: Apache License 2.0

Shell 0.15% JavaScript 5.13% TypeScript 94.72%

tangle's Introduction

Tangle Test Changes

JavaScript message bus implementation for various of different sandbox environments, e.g. worker threads, web workers, iframes, Visual Studio Code Webviews etc.

tangle allows to sync states between various components that live in different sandbox environments, e.g. iframes, webworkers, worker threads or VSCode webviews. It simplifies the communication between these sandboxes drastically. The package was mainly developed to help share data between various of webviews within a VSCode extension.

Demo

The ToDo list can be easily shared between 4 different iFrames. You can find this demo in the example directory.

Demo iFrame

Install

To install the package, run:

npm i --save tangle

or via Yarn:

yarn add tangle

Usage

This package allows to share a state or send events around various of environments. Make sure you use the correct import based on your use case:

  • to broadcast messages between various worker threads
    import Channel from 'tangle/worker_threads';
  • to broadcast messages between various web workers in the browser
    import Channel from 'tangle/webworkers';
  • to broadcast messages between VSCode webviews
    import Channel from 'tangle/webviews';
  • to broadcast messages between iFrames
    import Channel from 'tangle/iframes';
  • more to come...

State Management

Using the methods listen(eventName: keyof T, fn: Listener) and broadcast(state: T) you can share the state of an object. T defines the interface of an object representing the state. When initiating a channel you need to pass in the initialization value of that state, e.g.:

import Channel from 'tangle/worker_threads';

interface TodoList {
    todos: string[]
    dueDate: number
}

const ch = new Channel<TodoList>('foobar', {
    todos: [],
    dueDate: Date.now()
});

Now you can use listen to get notified when the state of a certain property changes and broadcast to update one or multiple properties of that state.

Example: Usage with Node.js Worker Threads

Within the parent thread, setup a message with a channel name and a default state:

import { Worker } from 'worker_threads';
import Channel from 'tangle/worker_threads';

const ch = new Channel('foobar', {
    counter: 0
});

/**
 * register message bus
 *
 * you can also work with it using RxJS Observables, e.g.:
 * ```js
 * const bus = ch.register([...]).subscribe((bus) => ...)
 * ```
 */
const bus = await ch.registerPromise([
    new Worker('./worker.js', { workerData: 'worker #1' }),
    new Worker('./worker.js', { workerData: 'worker #2' }),
    new Worker('./worker.js', { workerData: 'worker #3' })
])

bus.listen('counter', (counter) => console.log(`Counter update: ${counter}`))

Within the worker file you can attach to the message bus and share messages across all worker threads:

// in ./worker.js
import { workerData } from 'worker_threads';
import Channel from 'tangle/worker_threads';

const ch = new Channel('foobar', {
    counter: 0
});
const client = ch.attach();

if (workerData === 'worker #1') {
    client.broadcast({ counter: client.state.counter + 1 })
}
if (workerData === 'worker #2') {
    setTimeout(() => {
        client.broadcast({ counter: client.state.counter + 1 })
    }, 100)
}
if (workerData === 'worker #3') {
    setTimeout(() => {
        client.broadcast({ counter: client.state.counter + 1 })
    }, 200)
}

Given there are 3 worker threads attached to the message bus, the program would print:

Counter update: 0
Counter update: 1
Counter update: 2
Counter update: 3

Note: if you listen to a state property Tangle will immeditially emit the current value of that property.

Example: Usage with VSCode Webviews

When you initialize your extension and all your webviews and panels, create a message bus and attach all of them to it. If done successfully you can share events and states between panels and webviews, e.g.:

Demo VSCode

If you define a WebviewViewProvider expose a property webview that is an observable of an Webview. This is necessary because the web view can be loaded at any point in time, e.g.:

import {
    WebviewViewProvider,
    WebviewView,
    Webview,
    ExtensionContext,
    window
} from "vscode";
import Channel from 'tangle/webviews';

class PanelViewProvider implements WebviewViewProvider {
    private _webview = new Subject<Webview>();

    constructor(
        private readonly _context: ExtensionContext,
        public readonly identifier: string
    ) {}

    resolveWebviewView(webviewView: WebviewView) {
        /**
         * trigger channel initiation
         */
        this._webview.next(webviewView.webview)

        // ...
    }

    public static register(context: ExtensionContext, identifier: string) {
        const panelProvider = new PanelViewProvider(context, identifier);
        context.subscriptions.push(window.registerWebviewViewProvider(identifier, panelProvider));
        return panelProvider;
    }

    /**
     * expose webview subject as observable to that the Tangle channel is
     * initiated once the webview exists
     */
    public get webview() {
        return this._webview.asObservable();
    }
}

In or extension activation method we can then pass in the Webview directly, e.g. if created through createWebviewPanel or as observable.

export async function activate (context: vscode.ExtensionContext) {
    const ch = new Channel('vscode_state', { ... });
    const bus = await ch.registerPromise([
        vscode.window.createWebviewPanel(...).webview,
        vscode.window.createWebviewPanel(...).webview,
        PanelViewProvider.register(context, 'panel1').webview
    ])
}

Within the webviews you can assign to the same channel and send messages across. The attach method takes the instance of the webview which you receive by calling acquireVsCodeApi, e.g.:

import Channel from 'tangle/webviews';

const vscode = acquireVsCodeApi()
const ch = new Channel('vscode_state', {});
const client = ch.attach(vscode);

client.broadcast({ ... });

You can find more examples for other environments in the examples folder.

Event Handling

As oppose to managing state between JavaScript sandboxes you can use Tangle to just send around events among these environments. For this usecase, no default value for the state is necessary. The interface for event handling is the same as defined for the Node.js EventEmitter, e.g.:

import { Worker } from 'worker_threads';
import Channel from 'tangle/worker_threads';

const ch = new Channel('foobar');
const bus = await ch.registerPromise([
    new Worker('./worker.js', { workerData: 'worker #1' }),
    new Worker('./worker.js', { workerData: 'worker #2' }),
    new Worker('./worker.js', { workerData: 'worker #3' })
])

/**
 * listen to events using `on` or `once`
 */
bus.on('someEvent', (payload) => console.log(`New event: ${payload}`))
// in ./worker.js
import { workerData } from 'worker_threads';
import Channel from 'tangle/worker_threads';

const ch = new Channel('foobar');
const client = ch.attach();
client.emit('someEvent', workerData)

Given there are 3 worker threads attached to the message bus, the program would print:

worker #2
worker #1
worker #3

Lifecycle Considerations & Hooks

When clients attach to the channel hosted by the bus latency can be introduced by the time it takes sandbox environments (iframes, webworkers, worker threads or VSCode webviews) to create the runtime and load the scripts. This can create a problem if you are pushing state updates or events from the bus into the clients when completeness or order matters.

Bus will provide a whenReady(): Promise<Context> method that resolves (promise) when all clients have connected and as a result are ready to receive messages. Alternatively, you can also subscribe to the RxJS Observable Bus.context.subscribe(context => ...) to get notified when clients connect.

import { Worker } from "worker_threads";
import Channel from "tangle/worker_threads";

const ch = new Channel("foobar", {
    counter: 0,
});

// register message bus and broadcast when ready
const bus = await ch.registerPromise([
    // reuse worker.js above
    new Worker("./worker.js", { workerData: "worker #1" }),
    new Worker("./worker.js", { workerData: "worker #2" }),
    new Worker("./worker.js", { workerData: "worker #3" }),
]);

// set counter to 100 once all clients are ready
bus.whenReady().then(() => {
    bus.broadcast({ counter: 100 });
});

bus.listen("counter", (counter) => console.log(`Counter update: ${counter}`));

Typed Event Handling

Similar with state management you can define types for events and their payloads, e.g.:

interface Events {
    foo: string
    bar: number
}

const ch = new Channel<Events>('foobar');
const client = ch.attach();
client.emit('foo', 'bar')
client.emit('bar', true) // ๐Ÿšจ fails with "Argument of type 'number' is not assignable to parameter of type 'boolean'.ts(2345)"
client.emit('foobar', true) // ๐Ÿšจ fails with "Argument of type '"foobar"' is not assignable to parameter of type 'keyof Events'.ts(2345)"

Contribute

You have an idea on how to improve the package, please send us a pull request! Have a look into our contributing guidelines.

Getting Help

Running into issues or are you missing a specific usecase? Feel free to file an issue.


Copyright 2022 ยฉ Stateful โ€“ Apache 2.0 License

tangle's People

Contributors

christian-bromann avatar sourishkrout avatar dependabot[bot] avatar stateful-wombot avatar admc avatar

Stargazers

Roman avatar

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.