-
Notifications
You must be signed in to change notification settings - Fork 0
Starter's Guide
This guide will serve as a short and sweet introduction to the CommandSocket framework, including the use of both a server and client.
- Before Getting Started
- Creating the Server
- Creating the Client
- Creating & Utilizing Command Registries TODO
- Invoking Commands on the Server TODO
- Invoking Commands on the Client TODO
- Managing Clients TODO
Before we get started, we need to go over a couple of important key concepts of the CommandSocket framework.
First off, we have commands. Commands are the most important piece of the puzzle, and the Command
interface is going to be the one that you will probably be implementing the most. A CommandSocket Command
is a structure for defining executable code segments that have well defined inputs and outputs.
Every Command
implementation has an #execute
function as a part of its definition. The interface's #execute
member is defined as follows:
execute(params: C["params"], context: CommandSocket): Promise<C["return"]>;
Let's clarify a couple things about this function signature...
First off, the params: C["params"]
and Promise<C["return"]>
part. Don't worry too much about exactly what the type of the generic type C
is. All you need to know for now is that the params
parameter is of a type that can be defined for each individual Command
implementation, and the same goes for the return type of the function, although said return type does always need to be a Promise
. That said, the generic type of the Promise
can be specified.
Commands also have a simple #getName
method to implement that should simply return the full command identifier.
getName(): N;
The N
generic type can be specified as a generic argument to the Command
class just to provide some additional type-checking power, but can also be easily omitted for N
to default to the type string
.
Command registries are dynamic collections of commands that serve to inform each CommandSocket what commands are available to it. Command registries are formalized in the CommandRegistry
class, and has such methods as #addCommands
, #hasCommand
, and #getCommand
which are mostly self-explanatory.
let commandSocketOrServer: CommandSocket | CommandSocketServer = /* init */;
commandSocketOrServer.getCommandRegistry().addCommands(new MyCommand());
// We can now call MyCommand on this CommandSocket or CommandSocketServer!
Command sets are pretty self-explanatory -- they are sets of commands for CommandSockets to utilize. Command sets are formalized in the CommandSet
interface. That interface looks something like the following:
interface CommandSet {
[commandName: string]: CommandStructure;
}
In case you're not familiar with TypeScript's indexable types, what this interface is specifying is that for any objects that implement this interface, that object's members must all have string (not number) identifiers, and must each be of type CommandStructure
.
Now, command structures are somewhat less self-explanatory, but equally as simple. Command structures are used to specify the parameter type(s) and return type for a given Command
. They are formalized in the CommandStructure
interface, which looks like:
interface CommandStructure {
readonly params: any;
readonly return: any;
}
These two interfaces, CommandSet
and CommandStructure
are used in combination to specify to other developers using the CommandSocket framework (in particular, those developers hoping to use the CommandSocket API you are publishing!) what commands are available to them, and what the inputs and outputs of those commands should look like.
As an example, here's what a simple command set might look like for a CommandSocket that performs certain math and logic operations:
let myCommandSet: CommandSet = {
"my-command-set sum": {
params: number[],
return: number
},
"my-command-set xor": {
params: [boolean[], boolean[]],
return: boolean[]
},
"my-command-set stats": {
params: number[],
return: {
mean: number,
median: number,
mode: number,
stdDev: number
}
}
}
If you're wondering why the command names/identifiers have all been prefixed with my-command-set
, this is because of the importance of namespacing, but we'll return to that later.
Please note that these classes have nothing to do with internally informing the CommandSocket framework as to what commands actually exist, and what those commands' parameter types and return types are. These classes are merely meant as contracts between API designers and API users that can be directly used with TypeScript's generics system to securely typecheck the commands that are being called between CommandSockets. It is your duty as the API developer to keep your API's command set up-to-date with the actual commands that you are registering with your CommandSockets.
OK, it's time to create the CommandSocket server! To do so, all we have to do it create a new instance of CommandSocketServer
from the @command-socket/server
package.
The constructor for the CommandSocketServer
class takes one argument: the port on which the server should be started.
// Import the CommandSocket server package:
import { CommandSocketServer } from "@command-socket/server";
// Create a new instance of the CommandSocketServer class, starting on port 3849:
let server: CommandSocketServer = new CommandSocketServer(3849);
Now, we can also optionally specify the LCS (local command set) and RCS (remote command set) of our CommandSocketServer
as generic arguments upon initialization. Both of these generic arguments must implement the CommandSet
interface.
The LCS (local command set) contains information about the commands that are* available locally. This is to say that the commands that are specified in the LCS can be executed by the remotely connected CommandSocket on the local CommandSocket.
The RCS (remote command set) is the opposite, and therefore contains information about the commands that are* available on the remote CommandSocket
* Please read: should be. This behavior is more closely covered in the section on Command Sets & Command Structures.
If we wish to use generics, we can do so as follows:
// Initialize the CommandSocketServer with generic arguments.
let server: CommandSocketServer<MyLCS, MyRCS> = new CommandSocketServer(3849);
In case you missed the intro to command sets, you can find that above, under Command Sets & Command Structures.
Now, when I said that:
The constructor for the
CommandSocketServer
class takes one argument...
I lied by omission - the CommandSocketServer
class actually takes two arguments, the second parameter being a pre-initialized CommandRegistry
. In this guide we haven't actually covered how to do this yet though, but we will (after learning how to initialize the client (which also actually has the same second parameter))! If you'd rather skip to the section on creating and adding CommandRegistries, you can find that here: LINK
Now that we have our server ready, we can go ahead and create our CommandSocket client. There are clients available for the two big environments: NodeJS (@command-socket/node-client
) and the browser (@command-socket/browser-client
). But don't worry, both of these clients work in the exact same way, expose the same methods, and are overall interchangeable so long as they are used in their supported environment.
// Import the CommandSocket server package:
import { CommandSocket as CommandSocketClient } from "@command-socket/browser-client";
const PORT: number = 3849;
const HOST: string = "localhost";
const WS_ADDRESS: string = "ws://" + HOST + ":" + PORT;
// Create a new instance of the CommandSocketServer class, starting on port 3849:
let client: CommandSocketClient = new CommandSocketClient(WS_ADDRESS);
Note that we can also specify a local and remote Command Set like we can with a CommandSocket server.
// Initialize the CommandSocketClient with generic arguments.
let client: CommandSocketClient<MyLCS, MyRCS> = new CommandSocketClient(WS_ADDRESS);
The CommandSocketClient
will connect immediately on initialization.
OK, now that