Share types between serve and client, add type safety to messages (#213)

* Share types between serve and client, add type safety to messages

* Reverse client/server
This commit is contained in:
Stephen Weatherford 2017-11-16 15:29:58 -08:00 коммит произвёл GitHub
Родитель 405a6811dd
Коммит fa2e919b2b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 117 добавлений и 91 удалений

Просмотреть файл

@ -12,32 +12,12 @@ import { setInterval } from 'timers';
import { GraphConfiguration } from './GraphConfiguration';
import * as gremlin from "gremlin";
import { removeDuplicatesById } from "../utils/array";
import { GraphViewServerSocket } from "./GraphViewServerSocket";
import { Socket } from 'net';
let maxVertices = 300;
let maxEdges = 1000;
interface Edge {
id: string;
type: "edge";
outV: string; // Edge source ID
inV: string; // Edge target ID
};
interface Vertex {
id: string;
type: "vertex";
};
type Results = {
fullResults: any[];
countUniqueVertices: number;
countUniqueEdges: number;
// Limited by max
limitedVertices: Vertex[];
limitedEdges: Edge[];
};
function truncateWithEllipses(s: string, maxCharacters) {
if (s && s.length > maxCharacters) {
return `${s.slice(0, maxCharacters)}...`;
@ -59,10 +39,10 @@ export class GraphViewServer extends EventEmitter {
private _server: SocketIO.Server;
private _httpServer: http.Server;
private _port: number | undefined;
private _socket: SocketIO.Socket;
private _socket: GraphViewServerSocket;
private _previousPageState: {
query: string | undefined,
results: Results | undefined,
results: GraphResults | undefined,
errorMessage: string | undefined,
view: 'graph' | 'json',
isQueryRunning: boolean,
@ -127,7 +107,7 @@ export class GraphViewServer extends EventEmitter {
this._server.on('connection', socket => {
this.log(`Connected to client ${socket.id}`);
this._socket = socket;
this._socket = new GraphViewServerSocket(socket);
this.setUpSocket();
});
@ -138,7 +118,7 @@ export class GraphViewServer extends EventEmitter {
}
private async queryAndShowResults(queryId: number, gremlinQuery: string): Promise<void> {
var results: Results | undefined;
var results: GraphResults | undefined;
try {
this._previousPageState.query = gremlinQuery;
@ -178,20 +158,20 @@ export class GraphViewServer extends EventEmitter {
// If there's an error, send it to the client to display
var message = this.removeErrorCallStack(error.message || error.toString());
this._previousPageState.errorMessage = message;
this._socket.emit("showQueryError", queryId, message);
this._socket.emitToClient("showQueryError", queryId, message);
return;
} finally {
this._previousPageState.isQueryRunning = false;
}
this._socket.emit("showResults", queryId, results);
this._socket.emitToClient("showResults", queryId, results);
}
private getVertices(queryResults: any[]): Vertex[] {
private getVertices(queryResults: any[]): GraphVertex[] {
return queryResults.filter(n => n.type === "vertex" && typeof n.id === "string");
}
private limitVertices(vertices: Vertex[]): { countUniqueVertices: number, limitedVertices: Vertex[] } {
private limitVertices(vertices: GraphVertex[]): { countUniqueVertices: number, limitedVertices: GraphVertex[] } {
vertices = removeDuplicatesById(vertices);
let countUniqueVertices = vertices.length;
@ -200,11 +180,11 @@ export class GraphViewServer extends EventEmitter {
return { limitedVertices, countUniqueVertices };
}
private limitEdges(vertices: Vertex[], edges: Edge[]): { countUniqueEdges: number, limitedEdges: Edge[] } {
private limitEdges(vertices: GraphVertex[], edges: GraphEdge[]): { countUniqueEdges: number, limitedEdges: GraphEdge[] } {
edges = removeDuplicatesById(edges);
// Remove edges that don't have both source and target in our vertex list
let verticesById = new Map<string, Vertex>();
let verticesById = new Map<string, GraphVertex>();
vertices.forEach(n => verticesById.set(n.id, n));
edges = edges.filter(e => {
return verticesById.has(e.inV) && verticesById.has(e.outV);
@ -218,7 +198,7 @@ export class GraphViewServer extends EventEmitter {
return { limitedEdges, countUniqueEdges }
}
private async queryEdges(queryId: number, vertices: { id: string }[]): Promise<Edge[]> {
private async queryEdges(queryId: number, vertices: { id: string }[]): Promise<GraphEdge[]> {
// Split into multiple queries because they fail if they're too large
// Each of the form: g.V("id1", "id2", ...).outE().dedup()
// Picks up the outgoing edges of all vertices, and removes duplicates
@ -349,7 +329,7 @@ export class GraphViewServer extends EventEmitter {
this.log('getPageState');
if (this._previousPageState.query) {
this._socket.emit('setPageState', this._previousPageState);
this._socket.emitToClient('setPageState', this._previousPageState);
}
}
@ -371,28 +351,28 @@ export class GraphViewServer extends EventEmitter {
private handleGetTitleMessage() {
this.log(`getTitle`);
this._socket.emit('setTitle', `${this._configuration.databaseName} / ${this._configuration.graphName}`);
this._socket.emitToClient('setTitle', `${this._configuration.databaseName} / ${this._configuration.graphName}`);
}
private setUpSocket() {
this._socket.on('log', (...args: any[]) => {
this._socket.onClientMessage('log', (...args: any[]) => {
this.log('from client: ', ...args);
});
// Handle QueryTitle event from client
this._socket.on('getTitle', () => this.handleGetTitleMessage());
this._socket.onClientMessage('getTitle', () => this.handleGetTitleMessage());
// Handle query event from client
this._socket.on('query', (queryId: number, gremlin: string) => this.handleQueryMessage(queryId, gremlin));
this._socket.onClientMessage('query', (queryId: number, gremlin: string) => this.handleQueryMessage(queryId, gremlin));
// Handle state event from client
this._socket.on('getPageState', () => this.handleGetPageState());
this._socket.onClientMessage('getPageState', () => this.handleGetPageState());
// Handle setQuery event from client
this._socket.on('setQuery', (query: string) => this.handleSetQuery(query));
this._socket.onClientMessage('setQuery', (query: string) => this.handleSetQuery(query));
// Handle setView event from client
this._socket.on('setView', (view: 'graph' | 'json') => this.handleSetView(view));
this._socket.onClientMessage('setView', (view: 'graph' | 'json') => this.handleSetView(view));
}
private log(message, ...args: any[]) {

Просмотреть файл

@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as io from 'socket.io';
/**
* Wraps SocketIO.Socket to provide type safety
*/
export class GraphViewServerSocket {
constructor(private _socket: SocketIO.Socket) { }
public onClientMessage(event: ClientMessage, listener: Function): void {
this._socket.on(event, listener);
}
public emitToClient(message: ServerMessage, ...args: any[]): boolean {
// console.log("Message to client: " + message + " " + args.join(", "));
return this._socket.emit(message, ...args);
}
public disconnect(): void {
this._socket.disconnect();
}
}

Просмотреть файл

@ -1,5 +1,3 @@
import { error } from "util";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
@ -41,7 +39,7 @@ let htmlElements: {
type State = "empty" | "querying" | "error" | "json-results" | "graph-results";
type PageState = {
results: Results,
results: GraphResults,
isQueryRunning: boolean,
errorMessage?: string,
query: string,
@ -60,36 +58,14 @@ function logToUI(s: string) {
// htmlElements.debugLog.value = v;
}
type Results = {
fullResults: any[];
countUniqueVertices: number;
countUniqueEdges: number;
// Limited by max
limitedVertices: Vertex[];
limitedEdges: Edge[];
};
interface Edge {
id: string;
type: "edge";
outV: string; // Edge source ID
inV: string; // Edge target ID
};
interface Vertex {
id: string;
type: "vertex";
};
interface ForceNode {
vertex: Vertex;
vertex: GraphVertex;
x: number;
y: number;
}
interface ForceLink {
edge: Edge;
edge: GraphEdge;
source: ForceNode;
target: ForceNode;
}
@ -99,8 +75,21 @@ interface Point2D {
y: number;
}
class SocketWrapper {
constructor(private _socket: SocketIOClient.Socket) { }
public onServerMessage(message: ServerMessage | "connect" | "disconnect", fn: Function): SocketIOClient.Emitter {
return this._socket.on(message, fn);
}
public emitToHost(message: ClientMessage, ...args: any[]): SocketIOClient.Socket {
logToUI("Message to host: " + message + " " + args.join(", "));
return this._socket.emit(message, ...args);
}
}
export class GraphClient {
private _socket: SocketIOClient.Socket;
private _socket: SocketWrapper;
private _force: any;
private _currentQueryId = 0;
private _graphView: boolean;
@ -129,23 +118,23 @@ export class GraphClient {
this.setStateEmpty();
this.log(`Connecting on port ${port}`);
this._socket = io.connect(`http://localhost:${port}`);
this.log(`Listening on port ${port}`);
this._socket = new SocketWrapper(io.connect(`http://localhost:${port}`));
// setInterval(() => {
// this.log(`Client heartbeat on port ${port}: ${Date()}`);
// }, 10000);
this._socket.on('connect', (): void => {
this._socket.onServerMessage('connect', (): void => {
this.log(`Client connected on port ${port}`);
this._socket.emit('getTitle');
this._socket.emitToHost('getTitle');
});
this._socket.on('disconnect', (): void => {
this._socket.onServerMessage('disconnect', (): void => {
this.log("disconnect");
});
this._socket.on('setPageState', (pageState: PageState) => {
this._socket.onServerMessage("setPageState", (pageState: PageState) => {
htmlElements.queryInput.value = pageState.query;
if (pageState.isQueryRunning) {
@ -167,12 +156,12 @@ export class GraphClient {
}
});
this._socket.on('setTitle', (title: string): void => {
this._socket.onServerMessage("setTitle", (title: string): void => {
this.log(`Received title: ${title}`);
d3.select(htmlElements.title).text(title);
});
this._socket.on('showResults', (queryId: number, results: Results): void => {
this._socket.onServerMessage("showResults", (queryId: number, results: GraphResults): void => {
this.log(`Received results for query ${queryId}`);
if (queryId !== this._currentQueryId) {
@ -182,7 +171,7 @@ export class GraphClient {
}
});
this._socket.on('showQueryError', (queryId: number, error: string): void => {
this._socket.onServerMessage("showQueryError", (queryId: number, error: string): void => {
this.log(`Received error for query ${queryId} - ${error}`);
if (queryId !== this._currentQueryId) {
@ -194,12 +183,12 @@ export class GraphClient {
}
public getPageState() {
this.emitToHost('getPageState');
this._socket.emitToHost('getPageState');
}
public query(gremlin: string) {
this._currentQueryId += 1;
this.emitToHost("query", this._currentQueryId, gremlin);
this._socket.emitToHost("query", this._currentQueryId, gremlin);
this.setStateQuerying();
}
@ -215,7 +204,7 @@ export class GraphClient {
}
public setQuery(query: string) {
this.emitToHost('setQuery', query);
this._socket.emitToHost('setQuery', query);
}
private setView() {
@ -223,17 +212,12 @@ export class GraphClient {
htmlElements.jsonRadio.checked = !this._graphView;
d3.select(htmlElements.graphSection).classed("active", !!this._graphView);
d3.select(htmlElements.jsonSection).classed("active", !this._graphView);
this.emitToHost('setView', this._graphView ? 'graph' : 'json');
}
private emitToHost(message: string, ...args: any[]) {
logToUI("Message to host: " + message + " " + args.join(", "));
this._socket.emit(message, ...args);
this._socket.emitToHost('setView', this._graphView ? 'graph' : 'json');
}
private log(s: string) {
if (this._socket) {
this.emitToHost('log', s);
this._socket.emitToHost('log', s);
}
logToUI(s);
@ -270,7 +254,7 @@ export class GraphClient {
d3.select("#states").attr("class", fullState);
}
private showResults(results: Results): void {
private showResults(results: GraphResults): void {
// queryResults may contain any type of data, not just vertices or edges
// Always show the full original results JSON
@ -286,7 +270,7 @@ export class GraphClient {
this.displayGraph(results.countUniqueVertices, results.limitedVertices, results.countUniqueEdges, results.limitedEdges);
}
private splitVerticesAndEdges(nodes: any[]): [Vertex[], Edge[]] {
private splitVerticesAndEdges(nodes: any[]): [GraphVertex[], GraphEdge[]] {
let vertices = nodes.filter(n => n.type === "vertex");
let edges = nodes.filter(n => n.type === "edge");
return [vertices, edges];
@ -348,7 +332,7 @@ export class GraphClient {
+ " " + ux + "," + uy;
}
private displayGraph(countUniqueVertices: number, vertices: Vertex[], countUniqueEdges: number, edges: Edge[]) {
private displayGraph(countUniqueVertices: number, vertices: GraphVertex[], countUniqueEdges: number, edges: GraphEdge[]) {
try {
this.clearGraph();

Просмотреть файл

@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* These types are shared between server and client code
*/
interface GraphResults {
fullResults: any[];
countUniqueVertices: number;
countUniqueEdges: number;
// Limited by max
limitedVertices: GraphVertex[];
limitedEdges: GraphEdge[];
}
interface GraphEdge {
id: string;
type: "edge";
outV: string; // Edge source ID
inV: string; // Edge target ID
}
interface GraphVertex {
id: string;
type: "vertex";
}
// Messages that are sent from the server to the client
type ServerMessage = "setTitle" | "showResults" | "showQueryError" | "setPageState";
// Messages that are sent from the client to the server
type ClientMessage = "getPageState" | "getTitle" | "getPageState" | "query" | "setQuery" | "setView" | "log";