-
-
Save phiresky/bd9c4c1d089e2248d038e578f737d555 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); | |
}); | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
{ | |
"compilerOptions": { | |
"target": "es6", | |
"module": "commonjs", | |
"noUnusedLocals": true, | |
"noUnusedParameters": true | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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