Skip to content

Refactor Broadcast allowing for customizable transport stream decorators #59

Description

@shawncplus

The sty coloration library is currently hard coded inside the Broadcast class. Because Channel is a core class which uses Broadcast that makes core opinionated about how color should work. This makes it impossible to customize if you:

  • Don't want color
  • Don't like the sty syntax
  • Aren't using ANSI color

My high level concept is to create a system of customizable StreamDecorators that can be attached to TransportStreams in such a a way that a developer wouldn't have to modify core code AND wouldn't have to modify the transport bundle's (like telnet-networking code).

The difficulty is that TransportStream types aren't registered anywhere in the way that EntityLoaders are registered. This means there is currently no good central place to create the Stream+Decorators bindings.

Possible solutions

Bundle-centric

In this approach the transport bundle (e.g., telnet-networking) would entirely be in charge of this: they'd handle the initialization, customization, and management of decorators.

Pros

  • Maximum dev control

Cons

  • Little to no inter-bundle portability as every bundle might have a different configuration scheme, different decorator interfaces, etc.
  • All the work is foisted on the bundle dev
  • Likely to unintentionally imply that decorators should be hard coded into a transport bundle

Core-centric

In this approach there would be a base TransportDecorator class in core in the same way there is a base TransportStream class. Additionally the core would have to decide upon and own the configuration/binding strategy for Stream+Decorators

Pros

  • Easier on bundle developers
  • Easier for bundle users
  • Portable decorators
  • Unified configuration style

Cons

  • Less control for stream bundle developers since decorator configuration/binding will happen outside of their code
  • Having core do the configuration/binding means that the scheme will have to be more abstract and thereby potentially more complex than if it were up to the bundle itself

For me the core-centric approach seems like the obvious choice. The pieces of that puzzle look something like this:

TransportStream

TransportStream will need some way to identify themselves in such a way that configuration could link a transport stream to its decorators, my initial idea is that the class should get a new identifier getter, for example:

class TelnetStream extends TransportStream
{
  static get identifier() {
    return 'telnet';
  }

  // ...
}

TransportDecoratorRegistry

My idea for how to actually bind decorators to a stream involves creating a new TransportDecoratorRegistry. This class will be in charge of holding the configuration from ranvier.json which will look similar to DataLoader configuration:

// ranvier.json
{
  // register available decorators
  "transportDecorators": {
    "ansi": {
      // value follows same rules as DataLoader require config
      "require": "./lib/MyAnsiDecorator",
      // Serves as the default configuration for this decorator
      // which can be overridden by 'config' in the binding below
      "config": {
        "someDecoratorOption": 25
      }
    }
  },

  "transportDecoration": {
    "telnet": [
      {
        "decorator": "ansi",
        "config": {
          // additional configuration specific to this telnet:ansi binding
          "anotherOption": "foobar",
          // override of default config from registration
          "someDecoratorOption": 12,
        }
      }
    ]
  }
}

This is really verbose with all the configs but in practice it will look more like this:

{
  "streamDecorators": {
    "ansi": {
      "require": "cool-ansi-decorator"
    }
  },
  "streamDecoration": {
    "telnet": [
      { "decorator": "ansi" }
    ]
  }
}

To actually facilitate the usage of these decorators TransportDecoratorRegistry will have a decorate(TransportStream streamConstructor) method. This method will be used by transport bundle developers which means if a bundle developer so chooses they can not allow applying decorators:

class TransportDecoratorRegistry {
  decorate(streamConstructor) {
    const decorators = this.getDecorators(streamConstructor.identifier);
    return class extends streamConstructor {
      write(message, encoding) {
        message = decorators.reduce((acc, d) => d.decorate(acc), message);
        super.write(message, encoding);
      }
    };
  },
}

Writing a decorator

To write a new decorator, similar to DataLoader, there is no base class to extend. Instead you just need to follow the prescribed API:

const sty = require('sty');
class MyAnsiDecorator {
  // required configure(object config)
  configure(config = {}) {
    this.config = config;
  },

  // required decorate(string message)
  decorate(message) {
    return sty.parse(message);
  }
}

This class can either be an npm module or just a local file.

Changes for transport bundle devs

All that is in core. For the actual transport bundle developer they would write the following:

// inside their server-event script, e.g., server-events/telnet-server.js

// current:
const stream = new TelnetStream();
stream.attach(telnetSocket);

// new:
const stream = state.TransportDecoratorRegistry.decorate(TelnetStream);
stream.attach(telnetSocket);

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions