npm package: @hanseltime/janus-simple-command
Description: Initial Public Offering of Janus Simple Command Protocol Client 
Author: Justin Hanselman
Latest version: 1.4.0

# 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.

# Usage

Please see [Protocol](./PROTOCOL.md) 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.

```typescript
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
  connection,
  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) => {
      console.log(...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 }
console.log(result)
```

## 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.