mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 08:51:24 +00:00
358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
/**
|
|
* Lightweight gremlin client javascript library for the browser:
|
|
* - specs: http://tinkerpop.apache.org/docs/3.0.1-incubating/#_developing_a_driver
|
|
* - inspired from gremlin-javascript for nodejs: https://github.com/jbmusso/gremlin-javascript
|
|
* - tested on cosmosdb gremlin server
|
|
* - only supports sessionless gremlin requests
|
|
* - Relies on text-encoding polyfill (github.com/inexorabletash/text-encoding) for TextEncoder/TextDecoder on IE, Edge.
|
|
*/
|
|
|
|
import { TextEncoder, TextDecoder } from "text-encoding";
|
|
|
|
export interface GremlinSimpleClientParameters {
|
|
endpoint: string; // The websocket endpoint
|
|
user: string;
|
|
password: string;
|
|
successCallback: (result: Result) => void;
|
|
progressCallback: (result: Result) => void;
|
|
failureCallback: (result: Result, error: string) => void;
|
|
infoCallback: (msg: string) => void;
|
|
}
|
|
|
|
export interface Result {
|
|
requestId: string; // Can be null
|
|
data: any;
|
|
requestCharge: number; // RU cost
|
|
}
|
|
|
|
// Args are for Standard OpProcessor: sessionless requests
|
|
export interface GremlinRequestMessage {
|
|
requestId: string;
|
|
op: "eval" | "authentication";
|
|
processor: string;
|
|
args:
|
|
| {
|
|
gremlin: string;
|
|
bindings: {};
|
|
language: string;
|
|
}
|
|
| {
|
|
SASL: string;
|
|
};
|
|
}
|
|
|
|
export interface GremlinResponseMessage {
|
|
requestId: string;
|
|
status: {
|
|
attributes: {
|
|
/* The following fields are DEPRECATED. DO NOT USE.
|
|
StorageRU: string;
|
|
"x-ms-cosmosdb-graph-request-charge": number;
|
|
*/
|
|
|
|
"x-ms-request-charge": number;
|
|
"x-ms-total-request-charge": number;
|
|
};
|
|
code: number;
|
|
message: string;
|
|
};
|
|
result: {
|
|
data: any;
|
|
};
|
|
}
|
|
|
|
export class GremlinSimpleClient {
|
|
private static readonly requestChargeHeader = "x-ms-request-charge";
|
|
|
|
public params: GremlinSimpleClientParameters;
|
|
private protocols: string | string[];
|
|
private ws: WebSocket;
|
|
|
|
public requestsToSend: { [requestId: string]: GremlinRequestMessage };
|
|
public pendingRequests: { [requestId: string]: GremlinRequestMessage };
|
|
|
|
constructor(params: GremlinSimpleClientParameters) {
|
|
this.params = params;
|
|
this.pendingRequests = {};
|
|
this.requestsToSend = {};
|
|
}
|
|
|
|
public connect() {
|
|
if (this.ws) {
|
|
if (this.ws.readyState === WebSocket.CONNECTING) {
|
|
// Wait until it connects to execute all requests
|
|
return;
|
|
}
|
|
|
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
// Connection already open
|
|
this.executeRequestsToSend();
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.close();
|
|
|
|
const msg = `Connecting to ${this.params.endpoint} as ${this.params.user}`;
|
|
if (this.params.infoCallback) {
|
|
this.params.infoCallback(msg);
|
|
}
|
|
this.ws = GremlinSimpleClient.createWebSocket(this.params.endpoint);
|
|
this.ws.onopen = this.onOpen.bind(this);
|
|
this.ws.onerror = this.onError.bind(this);
|
|
this.ws.onmessage = this.onMessage.bind(this);
|
|
this.ws.onclose = this.onClose.bind(this);
|
|
this.ws.binaryType = "arraybuffer";
|
|
}
|
|
|
|
public static createWebSocket(endpoint: string): WebSocket {
|
|
return new WebSocket(endpoint);
|
|
}
|
|
|
|
public close() {
|
|
if (this.ws && this.ws.readyState !== WebSocket.CLOSING && this.ws.readyState !== WebSocket.CLOSED) {
|
|
const msg = `Disconnecting from ${this.params.endpoint} as ${this.params.user}`;
|
|
console.log(msg);
|
|
if (this.params.infoCallback) {
|
|
this.params.infoCallback(msg);
|
|
}
|
|
this.ws.close();
|
|
}
|
|
}
|
|
|
|
public decodeMessage(msg: MessageEvent): GremlinResponseMessage {
|
|
if (msg.data === null) {
|
|
return null;
|
|
}
|
|
|
|
if (msg.data.byteLength === 0) {
|
|
if (this.params.infoCallback) {
|
|
this.params.infoCallback("Received empty response");
|
|
}
|
|
return null;
|
|
}
|
|
try {
|
|
// msg.data is an ArrayBuffer of utf-8 characters, but handle string just in case
|
|
const data = typeof msg.data === "string" ? msg.data : new TextDecoder("utf-8").decode(msg.data);
|
|
return JSON.parse(data);
|
|
} catch (e) {
|
|
console.error(e, msg);
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(
|
|
null,
|
|
`Unexpected error while decoding backend response: ${e} msg:${JSON.stringify(msg)}`
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public onMessage(msg: MessageEvent) {
|
|
if (!msg) {
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(null, "onMessage called with no message");
|
|
}
|
|
return;
|
|
}
|
|
|
|
const rawMessage = this.decodeMessage(msg);
|
|
if (!rawMessage) {
|
|
return;
|
|
}
|
|
const requestId = rawMessage.requestId;
|
|
const statusCode = rawMessage.status.code;
|
|
const statusMessage = rawMessage.status.message;
|
|
|
|
const result: Result = {
|
|
requestId: requestId,
|
|
data: rawMessage.result ? rawMessage.result.data : null,
|
|
requestCharge: rawMessage.status.attributes[GremlinSimpleClient.requestChargeHeader]
|
|
};
|
|
|
|
if (!this.pendingRequests[requestId]) {
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(
|
|
result,
|
|
`Received response for missing or closed request: ${requestId} code:${statusCode} message:${statusMessage}`
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
switch (statusCode) {
|
|
case 200: // Success
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.successCallback) {
|
|
this.params.successCallback(result);
|
|
}
|
|
break;
|
|
case 204: // No content
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.successCallback) {
|
|
result.data = null;
|
|
this.params.successCallback(result);
|
|
}
|
|
break;
|
|
case 206: // Partial content
|
|
if (this.params.progressCallback) {
|
|
this.params.progressCallback(result);
|
|
}
|
|
break;
|
|
case 407: // Request authentication
|
|
const challengeResponse = this.buildChallengeResponse(this.pendingRequests[requestId]);
|
|
this.sendGremlinMessage(challengeResponse);
|
|
break;
|
|
case 401: // Unauthorized
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(result, `Unauthorized: ${statusMessage}`);
|
|
}
|
|
break;
|
|
case 498: // Malformed request
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(result, `Malformed request: ${statusMessage}`);
|
|
}
|
|
break;
|
|
case 500: // Server error
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(result, `Server error: ${statusMessage}`);
|
|
}
|
|
break;
|
|
case 597: // Script eval error
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(result, `Script eval error: ${statusMessage}`);
|
|
}
|
|
break;
|
|
case 598: // Server timeout
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(result, `Server timeout: ${statusMessage}`);
|
|
}
|
|
break;
|
|
case 599: // Server serialization error
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(result, `Server serialization error: ${statusMessage}`);
|
|
}
|
|
break;
|
|
default:
|
|
delete this.pendingRequests[requestId];
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(result, `Error with status code: ${statusCode}. Message: ${statusMessage}`);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is the main function to use in order to execute a GremlinQuery
|
|
* @param query
|
|
* @param successCallback
|
|
* @param progressCallback
|
|
* @param failureCallback
|
|
* @return requestId
|
|
*/
|
|
public executeGremlinQuery(query: string): string {
|
|
const requestId = GremlinSimpleClient.uuidv4();
|
|
this.requestsToSend[requestId] = {
|
|
requestId: requestId,
|
|
op: "eval",
|
|
processor: "",
|
|
args: {
|
|
gremlin: query,
|
|
bindings: {},
|
|
language: "gremlin-groovy"
|
|
}
|
|
};
|
|
this.connect();
|
|
return requestId;
|
|
}
|
|
|
|
public buildChallengeResponse(request: GremlinRequestMessage): GremlinRequestMessage {
|
|
var args = {
|
|
SASL: GremlinSimpleClient.utf8ToB64("\0" + this.params.user + "\0" + this.params.password)
|
|
};
|
|
return {
|
|
requestId: request.requestId,
|
|
processor: request.processor,
|
|
op: "authentication",
|
|
args
|
|
};
|
|
}
|
|
|
|
public static utf8ToB64(utf8Str: string) {
|
|
return btoa(
|
|
encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
|
|
return String.fromCharCode(parseInt(p1, 16));
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gremlin binary frame is:
|
|
* mimeLength + mimeType + serialized message
|
|
* @param requestMessage
|
|
*/
|
|
public static buildGremlinMessage(requestMessage: {}): Uint8Array {
|
|
const mimeType = "application/json";
|
|
let serializedMessage = mimeType + JSON.stringify(requestMessage);
|
|
const encodedMessage = new TextEncoder().encode(serializedMessage);
|
|
|
|
let binaryMessage = new Uint8Array(1 + encodedMessage.length);
|
|
binaryMessage[0] = mimeType.length;
|
|
|
|
for (let i = 0; i < encodedMessage.length; i++) {
|
|
binaryMessage[i + 1] = encodedMessage[i];
|
|
}
|
|
return binaryMessage;
|
|
}
|
|
|
|
private onOpen(event: any) {
|
|
this.executeRequestsToSend();
|
|
}
|
|
|
|
private executeRequestsToSend() {
|
|
for (let requestId in this.requestsToSend) {
|
|
const request = this.requestsToSend[requestId];
|
|
this.sendGremlinMessage(request);
|
|
this.pendingRequests[request.requestId] = request;
|
|
delete this.requestsToSend[request.requestId];
|
|
}
|
|
}
|
|
|
|
private onError(err: any) {
|
|
if (this.params.failureCallback) {
|
|
this.params.failureCallback(null, err);
|
|
}
|
|
}
|
|
|
|
private onClose(event: CloseEvent) {
|
|
this.requestsToSend = {};
|
|
this.pendingRequests = {};
|
|
|
|
if (event.wasClean) {
|
|
this.params.infoCallback(`Closed connection (${event.code} ${event.reason})`);
|
|
} else {
|
|
this.params.failureCallback(null, `Unexpectedly closed connection (${event.code} ${event.reason})`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* RFC4122 version 4 compliant UUID
|
|
*/
|
|
private static uuidv4() {
|
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
|
var r = (Math.random() * 16) | 0,
|
|
v = c == "x" ? r : (r & 0x3) | 0x8;
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
private sendGremlinMessage(gremlinRequestMessage: GremlinRequestMessage) {
|
|
const gremlinFrame = GremlinSimpleClient.buildGremlinMessage(gremlinRequestMessage);
|
|
this.ws.send(gremlinFrame);
|
|
}
|
|
}
|