Skip to content

Instantly share code, notes, and snippets.

@phiresky
Last active December 24, 2017 09:17
Show Gist options
  • Save phiresky/bd9c4c1d089e2248d038e578f737d555 to your computer and use it in GitHub Desktop.
Save phiresky/bd9c4c1d089e2248d038e578f737d555 to your computer and use it in GitHub Desktop.
/node_modules
import { RuntimeTestSchema } from "./0_exampleServer";
import { io } from "./clientUtil";
const client = io<RuntimeTestSchema>("http://localhost:3000");
client.on("connect", async () => {
console.log("connected");
const res = await client.emitAsync("getLength", "test");
// typeof res = { length: number }
console.log(res);
const res2 = await client.emitAsync("invalid", 123);
// Argument of type '"invalid"' is not assignable to parameter of type '"getLength" | ...'.
try {
await client.emitAsync("getLength", 123);
} catch (e) {
// Server side message: Invalid value 123 supplied to : string
console.log(e);
}
});
import socketio = require("socket.io");
import * as t from "io-ts";
import http = require("http");
import { Server, ClientSocketHandler, IClientSocketHandler, ToRuntime } from "./TypedServer";
import { ServerSideListenNamespaceOf } from "./typedSocket";
export const TestSchema = {
ServerMessages: {},
ClientMessages: {},
ClientRPCs: {
getLength: {
request: t.string,
response: t.strict({ length: t.number })
}
}
};
export type RuntimeTestSchema = ToRuntime<typeof TestSchema>;
class TestHandler extends ClientSocketHandler<typeof TestSchema>
implements IClientSocketHandler<typeof TestSchema> {
async getLength(x: string) {
// x is guaranteed to be a string
return { length: x.length };
}
}
const server = http.createServer();
const io = (socketio(server) as any) as ServerSideListenNamespaceOf<RuntimeTestSchema>;
server.listen(3000);
new Server(io, TestSchema, TestHandler);
import * as socketio from "socket.io-client";
import { ClientSideSocketOf, Schema } from "./typedSocket";
export function io<S extends Schema>(url: string) {
const c = socketio(url) as ClientSideSocketOf<S>;
promisifySocket(c);
return c;
}
export function promisifySocket(socket: ClientSideSocketOf<any>) {
socket.emitAsync = function(type: string, ...args: any[]) {
return new Promise((resolve, reject) => {
(socket as any).emit(type, ...args, (err: any, res: any) => {
if (err) reject(err);
else resolve(res);
});
});
};
}
{
"dependencies": {
"@types/node": "^8.0.50",
"@types/socket.io": "^1.4.31",
"io-ts": "^0.8.1",
"socket.io": "^2.0.4",
"socket.io-client": "^2.0.4"
}
}
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
/**
* typed socket.io server (runtime component)
*/
import * as t from "io-ts";
import * as ts from "./typedSocket";
import { isLeft } from "fp-ts/lib/Either";
import { PathReporter } from "io-ts/lib/PathReporter";
export type RuntimeSchema = {
ServerMessages: { [name: string]: t.Type<any> };
ClientMessages: { [name: string]: t.Type<any> };
ClientRPCs: {
[name: string]: {
request: t.Type<any>;
response: t.Type<any>;
};
};
};
export type ToRuntime<S extends RuntimeSchema> = {
ServerMessages: { [k in keyof S["ServerMessages"]]: t.TypeOf<S["ServerMessages"][k]> };
ClientMessages: { [k in keyof S["ClientMessages"]]: t.TypeOf<S["ClientMessages"][k]> };
ClientRPCs: {
[k in keyof S["ClientRPCs"]]: {
request: t.TypeOf<S["ClientRPCs"][k]["request"]>;
response: t.TypeOf<S["ClientRPCs"][k]["response"]>;
error: any;
}
};
};
type ClientMessagesHandler<S extends RuntimeSchema> = {
[k in keyof S["ClientMessages"]]: (message: t.TypeOf<S["ClientMessages"][k]>) => void
};
type ClientRPCsHandler<S extends RuntimeSchema> = {
[k in keyof S["ClientRPCs"]]: (
message: t.TypeOf<S["ClientRPCs"][k]["request"]>
) => Promise<t.TypeOf<S["ClientRPCs"][k]["response"]>>
};
export type IClientSocketHandler<S extends RuntimeSchema> = {
socket: ts.ServerSideClientSocketOf<ToRuntime<S>>;
io: ts.ServerSideListenNamespaceOf<ToRuntime<S>>;
} & ClientMessagesHandler<S> &
ClientRPCsHandler<S>;
export class ClientSocketHandler<Schema extends RuntimeSchema> {
constructor(
readonly io: ts.ServerSideListenNamespaceOf<ToRuntime<Schema>>,
readonly socket: ts.ServerSideClientSocketOf<ToRuntime<Schema>>
) {}
}
type ClientSocketHandlerConstructor<Schema extends RuntimeSchema> = new (
io: ts.ServerSideListenNamespaceOf<ToRuntime<Schema>>,
socket: ts.ServerSideClientSocketOf<ToRuntime<Schema>>
) => IClientSocketHandler<Schema>;
export class Server<Schema extends RuntimeSchema> {
_T: ToRuntime<Schema>;
constructor(
readonly io: ts.ServerSideListenNamespaceOf<ToRuntime<Schema>>,
schema: Schema,
clientSocketHandler: ClientSocketHandlerConstructor<Schema>
) {
this.io.on("connection", socket => {
//this.onConnection(socket);
const handler = new clientSocketHandler(this.io, socket);
for (const clientMessage in schema.ClientMessages) {
socket.on(clientMessage, (...args: any[]) =>
this.safeHandleClientMessage(
handler,
clientMessage,
args,
schema.ClientMessages[clientMessage]
)
);
}
for (const clientRPC in schema.ClientRPCs) {
socket.on(clientRPC, (...args: any[]) =>
this.safeHandleClientRPC(
handler,
clientRPC,
args,
schema.ClientRPCs[clientRPC]["request"]
)
);
}
});
}
sendClientError(socket: ts.ServerSideClientSocketOf<ToRuntime<Schema>>, e: any) {
socket.emit("typeError", e);
}
private safeHandleClientMessage(
handler: IClientSocketHandler<Schema>,
message: keyof Schema["ClientMessages"],
args: any[],
schema: t.Type<any>
) {
if (args.length !== 1) {
this.sendClientError(handler.socket, "Invalid arguments l " + args.length);
return;
}
const arg = args[0];
const validation = t.validate(arg, schema);
if (isLeft(validation)) {
const error = PathReporter.report(validation).join("\n");
console.error(handler.socket.id, message, error);
this.sendClientError(handler.socket, message + ": Type Error");
return;
}
const safeArg = validation.value;
try {
handler[message](safeArg);
return;
} catch (e) {
console.log(handler.socket.id, message, e);
}
}
private async safeHandleClientRPC(
handler: IClientSocketHandler<Schema>,
message: keyof Schema["ClientMessages"],
args: any[],
schema: t.Type<any>
) {
if (args.length !== 2) {
this.sendClientError(handler.socket, "Invalid arguments l " + args.length);
return;
}
const [arg, cb] = args;
if (typeof cb !== "function") {
this.sendClientError(handler.socket, message + ": No callback");
return;
}
const validation = t.validate(arg, schema);
if (isLeft(validation)) {
const error = PathReporter.report(validation).join("\n");
console.error(handler.socket.id, message, error);
cb(message + ": Type Error");
return;
}
const safeArg = validation.value;
try {
cb(null, await handler[message](safeArg));
} catch (e) {
console.log(handler.socket.id, message, e);
cb(e);
}
}
}
/**
* typed socket.io server (compile-time component)
*/
interface ClientRPCStructure {
[name: string]: {
request: any;
response: any;
error: any;
};
}
export type Schema = {
ServerMessages: { [name: string]: any };
ClientMessages: { [name: string]: any };
ClientRPCs: ClientRPCStructure;
};
type GeneralServerMessages = {
connect: void;
disconnect: string;
error: any;
};
type GeneralClientMessages = {
disconnect: void;
};
export interface ClientSideSocket<
ServerMessages,
ClientMessages,
ClientRPCs extends ClientRPCStructure
> {
on<K extends keyof (ServerMessages & GeneralServerMessages)>(
type: K,
listener: (info: (ServerMessages & GeneralServerMessages)[K]) => void
): this;
off<K extends keyof (ServerMessages & GeneralServerMessages)>(
type: K,
listener: (info: (ServerMessages & GeneralServerMessages)[K]) => void
): this;
once<K extends keyof (ServerMessages & GeneralServerMessages)>(
type: K,
listener: (info: (ServerMessages & GeneralServerMessages)[K]) => void
): this;
removeListener<K extends keyof (ServerMessages & GeneralServerMessages)>(
type: K,
listener: (info: (ServerMessages & GeneralServerMessages)[K]) => void
): this;
emit<K extends keyof ClientMessages>(type: K, info: ClientMessages[K]): this;
emit<K extends keyof ClientRPCs>(
type: K,
info: ClientRPCs[K]["request"],
callback: (error?: ClientRPCs[K]["error"], data?: ClientRPCs[K]["response"]) => void
): this;
emitAsync<K extends keyof ClientRPCs>(
type: K,
info: ClientRPCs[K]["request"]
): Promise<ClientRPCs[K]["response"]>;
connected: boolean;
connect(): any;
disconnect(): any;
removeAllListeners(): void;
}
export interface ServerSideListenNamespace<
ServerMessages,
ClientMessages,
ClientRPCs extends ClientRPCStructure
> {
on(
type: "connection",
listener: (info: ServerSideClientSocket<ServerMessages, ClientMessages, ClientRPCs>) => void
): this;
emit<K extends keyof ServerMessages>(type: K, info: ServerMessages[K]): this;
in(roomName: string): ServerSideListenNamespace<ServerMessages, ClientMessages, ClientRPCs>;
to(roomName: string): ServerSideListenNamespace<ServerMessages, ClientMessages, ClientRPCs>;
toUser(
userId: string | number
): ServerSideListenNamespace<ServerMessages, ClientMessages, ClientRPCs>;
close(): void;
// of<K extends keyof KnownNamespaces>(ns: K): ServerSideListenNamespaceOf<K>;
}
export interface ServerSideClientSocket<
ServerMessages,
ClientMessages,
ClientRPCs extends ClientRPCStructure
> {
disconnect(): void;
id: string;
on<K extends keyof (ClientMessages & GeneralClientMessages)>(
type: K,
listener: (info: (ClientMessages & GeneralClientMessages)[K]) => void
): this;
on<K extends keyof ClientRPCs>(
type: K,
listener: (
info: ClientRPCs[K]["request"],
callback: (
error: ClientRPCs[K]["error"] | null,
data?: ClientRPCs[K]["response"]
) => void
) => void
): this;
onAsync<K extends keyof ClientRPCs>(
type: K,
listener: (info: ClientRPCs[K]["request"]) => Promise<ClientRPCs[K]["response"]>
): this;
emit<K extends keyof ServerMessages>(type: K, info: ServerMessages[K]): this;
}
export type ServerSideListenNamespaceOf<X extends Schema> = ServerSideListenNamespace<
X["ServerMessages"],
X["ClientMessages"],
X["ClientRPCs"]
>;
export type ServerSideClientSocketOf<X extends Schema> = ServerSideClientSocket<
X["ServerMessages"],
X["ClientMessages"],
X["ClientRPCs"]
>;
export type ClientSideSocketOf<X extends Schema> = ClientSideSocket<
X["ServerMessages"],
X["ClientMessages"],
X["ClientRPCs"]
>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment