@savantly/gremlin-js
Version:
Gremlin client [Tinkerpop]
199 lines • 7.45 kB
JavaScript
import { Graphson } from './graphson';
import { GremlinQueryOperation } from './gremlin.query.operation';
import { GremlinQueryResponse } from './gremlin.query.response';
import { assertNotEmpty } from './utils';
export class GremlinWebSocket {
constructor(options) {
this.options = options;
this._queries = {};
this._queue = new Array();
this.maxReconnects = 30;
this.reconnectAttempts = 0;
this.open();
}
isOpen() {
return this._ws && this._ws.OPEN === this._ws.readyState;
}
execute(gremlinQuery) {
assertNotEmpty(gremlinQuery.id);
this._queries[gremlinQuery.id] = gremlinQuery;
const graphson = this.queryToGraphson(gremlinQuery, GremlinQueryOperation.eval);
this._queue.push(graphson);
if (this.isOpen()) {
this.executeQueue();
}
}
sendBinaryMessage(binaryData) {
this._ws.send(binaryData);
}
/**
* Process the current command queue, sending commands to Gremlin Server
* (First In, First Out).
*/
executeQueue() {
while (this._queue.length > 0) {
const graphson = this._queue.shift();
const binaryData = this.graphsonToBinary(graphson);
this.sendBinaryMessage(binaryData);
}
}
cancelPendingCommands({ message, details }) {
this._queue.length = 0;
this._queries = {};
}
arrayBufferToString(buffer) {
console.log('converting buffer to string');
const bufView = new Uint8Array(buffer);
const length = bufView.length;
let result = '';
let addition = Math.pow(2, 8) - 1;
for (let i = 0; i < length; i += addition) {
if (i + addition > length) {
addition = length - i;
}
result += String.fromCharCode.apply(null, bufView.subarray(i, i + addition));
}
console.log('extracted string from buffer: ' + result);
return result;
}
/*
* Process all incoming raw message events sent by Gremlin Server, and dispatch
* to the appropriate command.
*
*/
onMessage(message) {
let rawMessage;
let requestId;
let statusCode;
let statusMessage;
console.log('web socket received message');
try {
const { data } = message;
const rawMessageString = this.arrayBufferToString(data);
rawMessage = JSON.parse(rawMessageString);
requestId = rawMessage.requestId;
statusCode = rawMessage.status.code;
statusMessage = rawMessage.status.message;
}
catch (e) {
console.warn('MalformedResponse', 'Received malformed response message');
console.log(message);
return;
}
const gremlinResponse = new GremlinQueryResponse();
gremlinResponse.rawMessage = rawMessage;
gremlinResponse.requestId = requestId;
gremlinResponse.statusCode = statusCode;
gremlinResponse.statusMessage = statusMessage;
console.log('preparing to excecute callback for request');
// If we didn't find a waiting query for this response, emit a warning
if (!this._queries[requestId]) {
console.warn('OrphanedResponse', `Received response for missing or closed request: ${requestId}, status: ${statusCode}, ${statusMessage}`);
return;
}
const query = this._queries[requestId];
switch (statusCode) {
case 200:// SUCCESS
delete this._queries[requestId]; // TODO: optimize performance
query.onMessage(gremlinResponse);
query.onMessage(null);
break;
case 204:// NO_CONTENT
delete this._queries[requestId];
query.onMessage(null);
break;
case 206:// PARTIAL_CONTENT
query.onMessage(rawMessage);
break;
case 407:// AUTHENTICATE CHALLANGE
const challengeResponse = this.buildChallengeResponse(query);
this._queue.push(challengeResponse);
this.executeQueue();
break;
default:
delete this._queries[requestId];
console.error(statusMessage + ' (Error ' + statusCode + ')');
break;
}
}
onClose(evt) {
console.log('connection closed');
if (this._queue.length > 0) {
console.warn(`${this._queue.length} queries are queued`);
}
if (this.reconnectAttempts < this.maxReconnects && !this.open()) {
this.reconnect();
}
else if (!this.open()) {
console.warn('giving up reconnecting');
}
else {
this.reconnectAttempts = 0;
}
}
reconnect() {
setTimeout(() => {
console.log('reconnecting');
this.onClose(null);
}, 1000);
}
onOpen(evt) {
console.log('opened connection');
this.executeQueue();
}
onError(err) {
console.log('an error occured');
console.error(err);
}
buildChallengeResponse(query) {
const { processor, accept, language } = this.options;
const saslbase64 = btoa('\0' + this.options.user + '\0' + this.options.password);
const args = { sasl: saslbase64 };
const message = {
requestId: query.id,
processor,
op: GremlinQueryOperation.authentication,
args,
};
const graphson = new Graphson(message);
return graphson;
}
queryToGraphson(query, operation) {
console.log(`queryToGraphson: ${query.id}`);
return query.getGraphson(operation);
}
/*
* returns a binary format ready for web-socket transfer
*/
graphsonToBinary(query) {
const serializedMessage = this.options.accept + JSON.stringify(query);
// Let's start packing the message into binary
// mimeLength(1) + mimeType Length + serializedMessage Length
const binaryMessage = new Uint8Array(1 + serializedMessage.length);
binaryMessage[0] = this.options.accept.length;
for (let i = 0; i < serializedMessage.length; i++) {
binaryMessage[i + 1] = serializedMessage.charCodeAt(i);
}
return binaryMessage;
}
isConnecting() {
return this._ws && this._ws.readyState === this._ws.CONNECTING;
}
/*
* return true if open or connecting
*/
open() {
if (this.isOpen() || this.isConnecting()) {
return true;
}
const address = `ws${this.options.ssl ? 's' : ''}://${this.options.host}:${this.options.port}${this.options.path}`;
this._ws = new WebSocket(address);
this._ws.binaryType = 'arraybuffer';
this._ws.onopen = (evt) => { this.onOpen(evt); };
this._ws.onerror = (evt) => { this.onError(evt); };
this._ws.onmessage = (evt) => { this.onMessage(evt); };
this._ws.onclose = (evt) => { this.onClose(evt); };
return false;
}
}
//# sourceMappingURL=gremlin.web.socket.js.map