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:
Родитель
405a6811dd
Коммит
fa2e919b2b
|
@ -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";
|
Загрузка…
Ссылка в новой задаче