From fa2e919b2b5ba2e33c892cd7fd28e2ea8ca14380 Mon Sep 17 00:00:00 2001 From: Stephen Weatherford Date: Thu, 16 Nov 2017 15:29:58 -0800 Subject: [PATCH] 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 --- src/graph/GraphViewServer.ts | 62 ++++++++-------------- src/graph/GraphViewServerSocket.ts | 26 +++++++++ src/graph/client/graphClient.ts | 84 ++++++++++++------------------ src/graph/client/graphTypes.ts | 36 +++++++++++++ 4 files changed, 117 insertions(+), 91 deletions(-) create mode 100644 src/graph/GraphViewServerSocket.ts create mode 100644 src/graph/client/graphTypes.ts diff --git a/src/graph/GraphViewServer.ts b/src/graph/GraphViewServer.ts index 45c81a6..4462c43 100644 --- a/src/graph/GraphViewServer.ts +++ b/src/graph/GraphViewServer.ts @@ -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 { - 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(); + let verticesById = new Map(); 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 { + private async queryEdges(queryId: number, vertices: { id: string }[]): Promise { // 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[]) { diff --git a/src/graph/GraphViewServerSocket.ts b/src/graph/GraphViewServerSocket.ts new file mode 100644 index 0000000..afebf8f --- /dev/null +++ b/src/graph/GraphViewServerSocket.ts @@ -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(); + } +} diff --git a/src/graph/client/graphClient.ts b/src/graph/client/graphClient.ts index f06601c..b36b7c8 100644 --- a/src/graph/client/graphClient.ts +++ b/src/graph/client/graphClient.ts @@ -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(); diff --git a/src/graph/client/graphTypes.ts b/src/graph/client/graphTypes.ts new file mode 100644 index 0000000..90f8c4f --- /dev/null +++ b/src/graph/client/graphTypes.ts @@ -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";