TypeScript icon, indicating that this package has built-in type declarations

1.4.0 • Public • Published

Janus Simple Command Library

This library provides both a "Client" and "Server" class for following the Janus Simple Command Protocol.

This protocol prioritizes human readable message passing and JSON formats over compact message structures. This is meant as a library for plugging into any number of bi-directional connections that you can define, and provides a means of multi-plexing multiple "senders" on the same connection.


Please see Protocol for an in depth explanation of the protocol as it stands.

In short, on a bidirectional connection either party can initiate a "sender" or a server. The server exists to simply respond to the requests of a sender. The sender talks with the server by commands and then waits for a status message that returns.

From the standpoint of this library, you only need to worry yourself with commands, their send payloads, and the corresponding status payloads that could come back.

import { CommandMap, CommandMessage, StatusMessage, IntermediateStatusMap, StatusMap } from '@hanseltime/janus-simple-command'
// Strongly type your expected commands
type Commands = 'fly' | 'eat'

// Define the command schemas using our built-in CommandMessage type
type CommandMap: CommandMap<Commands> = {
  fly: CommandMessage<'fly', {
    captain: string,
    plane: 'cessna' | '737'
  eat: CommandMessage<'eat', {
    eat: boolean
// Define the status schema using our built-in StatusMessage type
type FlySuccess = {
  mph: number
  elevation: number
type FlyErrors = 'crashed' | 'failureToLaunch'
type EatSuccess = {
  food: string
type StatusMap: StatusMap<Commands> = {
  fly: StatusMessage<FlySuccess, FlyErrors>
  eat: StatusMessage<EatSuccess>
// Optional - you can define intermediate statuses for a command
type InterMap: IntermediateStatusMap<Commands> = {
  fly: IntermediateStatusMessage<{
    phase: 'lift-off' | 'grounded'

// Create your connection
const connection: Connection = someConnectionFunction()

const server = new Server<Commands, CommandMap, StatusMap, InterMap>({
  maxSenderInactivity: 10000, // the amount of time we allow between commands
  maxAckRetries: 4, // If we drop an ack, the amount of times we retry a status
  ackRetryDelay: 500, // The amount we wait before retrying on a dropped ack
  debug: (msg) => console.log(msg),

server.addMessageHandler('fly', async (msg: CommandMap['fly'], sendIntermediateStatus ): Promise<HandlerReturn<StatusMap['fly']>> => {
  // Indicate that we're taking off while we do other stuff
  const sent = await sendIntermediateStatus({
    phase: 'lift-off'

  // If we don't verify it was sent we don't want to keep flying
  if (!sent) {
    console.error('pilot did not acknowledge, canceling flight')
    return {
      isError: true,
      data: {
        type: 'failureToLaunch',
        message: 'Pilot Failed to acknowledge',

  return {
    isError: false,
    data: {
      mph: msg.data.plane === 'cessna' ? 100 : 200,
      elevation: 30000,
// Handler for eat command
server.addMessageHandler('eat', async (msg: CommandMap['eat']): Promise<HandlerReturn<StatusMap['eat']>> => {
  // Return for eat

await server.open()

// At the end of the server
await server.close()

// For the client connection
const connectionForClient: Connection = someConnectionFunction()

const client = new Client<Commands, CommandMap, StatusMap>({
    commands: ['fly', 'eat'],
    ackRetryDelay: 1000,
    maxAckRetries: 3,
    connection: clientConnection,
    debug: (...args) => {

await client.open()

// Create a sender
const sender = await client.createSender()
const result = sender.command('fly', () => ({
  captain: 'jack',
  plane: '737',

// { mph: 200, elevation: 30000 }

Creating a Connection

In order to agnosticize our library's dependence on a transport layer, this library relies on a simplified connection interface. You can either look for other implementations of the connection or you can instantiate your own interface and pass it to the connection.

One-to-One Relationship

The Connection interface that you implement is meant to be 1-to-1 to either a Client or Server instance. When noting the setter methods, we only allow one set of listeners on the connection. This does not mean that you cannot run multiple Servers over the same underlying connection (like a websocket), but it does mean that you will provide a Connection that interacts with the websocket for each Server or Client that you want to declare.




Package Sidebar


npm i @hanseltime/janus-simple-command

Weekly Downloads






Unpacked Size

490 kB

Total Files


Last publish


  • hanseltime